feat(ui): 브랜드 '수습 플랫폼'→'학습 플랫폼', 비밀번호 눈 토글 추가
- 로고·타이틀·강좌 예시의 '수습(학습) 플랫폼' 표기를 '학습 플랫폼'으로 통일 - PasswordInput 컴포넌트: 비밀번호 칸 끝에 눈 버튼(👁️/🙈)으로 마스킹 토글 (오타 확인용) — type=button으로 폼 제출 방지, ...props로 기존 input 대체 - 로그인·비밀번호 변경(3칸)·회원가입(Field password 타입 2칸)에 일괄 적용 운영 검증: 타이틀 '학습 플랫폼' 확인, 눈 클릭 시 password→text 전환·아이콘 토글 브라우저 확인. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d8708caca5
commit
f571462b95
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AWESOMEDEV 수습 학습 플랫폼</title>
|
||||
<title>AWESOMEDEV 학습 플랫폼</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@ -97,7 +97,7 @@ function Layout() {
|
||||
<header className="topnav">
|
||||
<div className="topnav-inner">
|
||||
<div className="topnav-logo">
|
||||
AWESOMEDEV <span>수습 플랫폼</span>
|
||||
AWESOMEDEV <span>학습 플랫폼</span>
|
||||
</div>
|
||||
<nav className="topnav-menu">
|
||||
{/* 학습 포인트: NavLink는 현재 주소와 일치하면 isActive=true를 준다.
|
||||
|
||||
36
frontend/src/components/PasswordInput.jsx
Normal file
36
frontend/src/components/PasswordInput.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* 이 파일이 하는 일: 비밀번호 입력창 + 끝에 "눈" 토글 버튼.
|
||||
* 눈을 누르면 가려진 비밀번호(••••)를 잠깐 글자로 볼 수 있다(오타 확인용).
|
||||
*
|
||||
* 학습 포인트 ① — 왜 재사용 컴포넌트로 뽑나?
|
||||
* 로그인·회원가입·비밀번호 변경 등 비밀번호 칸이 여러 곳에 있다. 각자 눈 토글을
|
||||
* 복사하면 동작이 조금씩 어긋난다. 한 부품으로 만들어 두면 모든 칸이 똑같이 동작한다.
|
||||
*
|
||||
* 학습 포인트 ② — 왜 <input>을 그대로 감싸고 나머지 props는 통째로 넘기나(...props)?
|
||||
* 이 컴포넌트는 "type만 토글해 주는 얇은 껍데기"다. id·value·onChange·required 같은 건
|
||||
* 원래 input에 주던 그대로 넘겨받아 흘려보낸다 — 기존 코드에서 <input>만 이걸로 바꾸면 된다.
|
||||
*
|
||||
* 학습 포인트 ③ — type="button" 이 중요하다.
|
||||
* 폼 안의 버튼은 기본 type이 "submit"이라, 눈을 누르면 폼이 제출돼 버린다.
|
||||
* type="button"으로 명시해 "이 버튼은 제출이 아니라 그냥 클릭"임을 분명히 한다.
|
||||
*/
|
||||
export default function PasswordInput({ className = 'input', ...props }) {
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="password-wrap">
|
||||
<input {...props} type={show ? 'text' : 'password'} className={className} />
|
||||
<button
|
||||
type="button"
|
||||
className="password-toggle"
|
||||
onClick={() => setShow((s) => !s)}
|
||||
aria-label={show ? '비밀번호 숨기기' : '비밀번호 보기'}
|
||||
title={show ? '비밀번호 숨기기' : '비밀번호 보기'}
|
||||
>
|
||||
<span aria-hidden="true">{show ? '🙈' : '👁️'}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -8,6 +8,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import client from '../api/client';
|
||||
import PasswordInput from '../components/PasswordInput';
|
||||
|
||||
export default function ChangePasswordPage() {
|
||||
const navigate = useNavigate();
|
||||
@ -83,10 +84,8 @@ export default function ChangePasswordPage() {
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="field">
|
||||
<label htmlFor="cur-pw">현재 비밀번호</label>
|
||||
<input
|
||||
<PasswordInput
|
||||
id="cur-pw"
|
||||
className="input"
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
@ -95,10 +94,8 @@ export default function ChangePasswordPage() {
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="new-pw">새 비밀번호 (8자 이상)</label>
|
||||
<input
|
||||
<PasswordInput
|
||||
id="new-pw"
|
||||
className="input"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
@ -107,10 +104,8 @@ export default function ChangePasswordPage() {
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="new-pw2">새 비밀번호 확인</label>
|
||||
<input
|
||||
<PasswordInput
|
||||
id="new-pw2"
|
||||
className="input"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
|
||||
@ -375,7 +375,7 @@ export default function CodingBasicsPage() {
|
||||
>
|
||||
<ol style={{ paddingLeft: 20 }}>
|
||||
<li style={{ marginBottom: 6 }}>
|
||||
로그인 페이지의 로고 문구("AWESOMEDEV 수습 플랫폼")를 내 마음대로
|
||||
로그인 페이지의 로고 문구("AWESOMEDEV 학습 플랫폼")를 내 마음대로
|
||||
바꿔서 화면에 반영해 보기 — <code>LoginPage.jsx</code>
|
||||
</li>
|
||||
<li style={{ marginBottom: 6 }}>
|
||||
|
||||
@ -162,7 +162,7 @@ export default function FindAccountPage() {
|
||||
<div className="card" style={{ width: '100%', maxWidth: 380 }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 20 }}>
|
||||
<div className="topnav-logo" style={{ fontSize: 18 }}>
|
||||
AWESOMEDEV <span>수습 플랫폼</span>
|
||||
AWESOMEDEV <span>학습 플랫폼</span>
|
||||
</div>
|
||||
<p className="muted" style={{ fontSize: 14, marginTop: 6 }}>
|
||||
아이디·비밀번호 찾기
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, Navigate, Link } from 'react-router-dom';
|
||||
import { useAuth } from '../AuthContext';
|
||||
import PasswordInput from '../components/PasswordInput';
|
||||
|
||||
export default function LoginPage() {
|
||||
const { user, login } = useAuth();
|
||||
@ -52,7 +53,7 @@ export default function LoginPage() {
|
||||
<div className="card" style={{ width: '100%', maxWidth: 380 }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 24 }}>
|
||||
<div className="topnav-logo" style={{ fontSize: 18 }}>
|
||||
AWESOMEDEV <span>수습 플랫폼</span>
|
||||
AWESOMEDEV <span>학습 플랫폼</span>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
@ -70,10 +71,8 @@ export default function LoginPage() {
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="password">비밀번호</label>
|
||||
<input
|
||||
<PasswordInput
|
||||
id="password"
|
||||
className="input"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
|
||||
@ -6,6 +6,7 @@ import { useNavigate, Link, Navigate } from 'react-router-dom';
|
||||
import { useAuth } from '../AuthContext';
|
||||
import client from '../api/client';
|
||||
import { embedPostcode } from '../lib/postcode';
|
||||
import PasswordInput from '../components/PasswordInput';
|
||||
|
||||
const TRACKS = [
|
||||
{ value: 'DEV', label: '개발' },
|
||||
@ -41,18 +42,20 @@ const EMPTY = {
|
||||
* (밖으로 나오면서 form/set에 직접 접근할 수 없으니 value와 onChange를 props로 받는다.)
|
||||
*/
|
||||
function Field({ label, k, type = 'text', placeholder, autoComplete, value, onChange }) {
|
||||
// 공통 props — 일반 input과 비밀번호(눈 토글) 둘 다 같은 값을 받는다.
|
||||
const shared = {
|
||||
id: `su-${k}`,
|
||||
className: 'input',
|
||||
value,
|
||||
onChange: (e) => onChange(k, e.target.value),
|
||||
placeholder,
|
||||
autoComplete,
|
||||
};
|
||||
return (
|
||||
<div className="field">
|
||||
<label htmlFor={`su-${k}`}>{label}</label>
|
||||
<input
|
||||
id={`su-${k}`}
|
||||
className="input"
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={(e) => onChange(k, e.target.value)}
|
||||
placeholder={placeholder}
|
||||
autoComplete={autoComplete}
|
||||
/>
|
||||
{/* 비밀번호 칸이면 눈 토글이 달린 PasswordInput을 쓴다(가입 폼도 오타 확인 가능). */}
|
||||
{type === 'password' ? <PasswordInput {...shared} /> : <input {...shared} type={type} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -170,7 +173,7 @@ export default function SignupPage() {
|
||||
<div className="card" style={{ width: '100%', maxWidth: 460 }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 20 }}>
|
||||
<div className="topnav-logo" style={{ fontSize: 18 }}>
|
||||
AWESOMEDEV <span>수습 플랫폼</span>
|
||||
AWESOMEDEV <span>학습 플랫폼</span>
|
||||
</div>
|
||||
<p className="muted" style={{ fontSize: 14, marginTop: 6 }}>수습생 회원가입</p>
|
||||
</div>
|
||||
|
||||
@ -25,7 +25,7 @@ const CODE_SPECTRUM = `웹 페이지 ───────────── PWA
|
||||
|
||||
const CODE_MANIFEST = `// manifest.webmanifest — 브라우저에게 "나 이런 앱이야"라고 소개하는 명함
|
||||
{
|
||||
"name": "AWESOMEDEV 수습 학습 플랫폼",
|
||||
"name": "AWESOMEDEV 학습 플랫폼",
|
||||
"short_name": "미림학습", // 홈 화면 아이콘 아래 짧은 이름
|
||||
"start_url": "/", // 아이콘을 누르면 열릴 주소
|
||||
"display": "standalone", // 주소창 없이 '앱처럼' 전체화면
|
||||
@ -113,7 +113,7 @@ export default defineConfig({
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate', // 새 버전 배포 시 서비스워커 자동 교체
|
||||
manifest: {
|
||||
name: 'AWESOMEDEV 수습 학습 플랫폼',
|
||||
name: 'AWESOMEDEV 학습 플랫폼',
|
||||
short_name: '미림학습',
|
||||
display: 'standalone',
|
||||
theme_color: '#4f46e5',
|
||||
|
||||
@ -89,7 +89,7 @@ const CODE_COOKIE = `로그인 후 쿠키는 어디에? — Postman이 알아서
|
||||
|
||||
const CODE_COLLECTION = `컬렉션 = 요청들을 담아 두는 폴더 (즐겨찾기 + 실행 순서)
|
||||
|
||||
📁 AWESOMEDEV 수습 플랫폼 ← 컬렉션 (New → Collection)
|
||||
📁 AWESOMEDEV 학습 플랫폼 ← 컬렉션 (New → Collection)
|
||||
├─ ① POST /api/auth/login 로그인 (출입증 받기)
|
||||
├─ ② GET /api/checklist 내 체크리스트 조회
|
||||
├─ ③ POST /api/checklist/1/submit 항목 제출
|
||||
@ -354,7 +354,7 @@ export default function ToolPostmanPage() {
|
||||
무슨 순서로 실행해야 하는지 바로 보입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ③</b> "AWESOMEDEV 수습 플랫폼" 컬렉션을 만들고,
|
||||
<b>직접 확인해 보기 ③</b> "AWESOMEDEV 학습 플랫폼" 컬렉션을 만들고,
|
||||
섹션 3~4에서 보낸 요청들을 저장해 넣으세요. 그리고 Postman을 완전히 껐다 켠 뒤
|
||||
컬렉션에서 로그인부터 순서대로 다시 실행 — 어제의 실험을 오늘 그대로
|
||||
재현할 수 있다는 것, 이게 컬렉션의 힘이에요.
|
||||
|
||||
@ -331,6 +331,37 @@ a {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* 비밀번호 입력창 + 눈 토글 (PasswordInput) */
|
||||
.password-wrap {
|
||||
position: relative;
|
||||
}
|
||||
/* 눈 버튼이 앉을 자리를 오른쪽에 비워 둔다 — 글자가 버튼 밑으로 파고들지 않게 */
|
||||
.password-wrap .input {
|
||||
padding-right: 42px;
|
||||
}
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 6px;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 6px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.password-toggle:hover {
|
||||
background: var(--bg);
|
||||
}
|
||||
.password-toggle:focus-visible {
|
||||
outline: 2px solid var(--primary-soft);
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user