feat(ui): 브랜드 '수습 플랫폼'→'학습 플랫폼', 비밀번호 눈 토글 추가
All checks were successful
CI / backend-test (push) Successful in 59s
CI / frontend-build (push) Successful in 39s
CI / backend-dep-scan (push) Successful in 6s

- 로고·타이틀·강좌 예시의 '수습(학습) 플랫폼' 표기를 '학습 플랫폼'으로 통일
- 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:
AWESOMEDEV 2026-07-20 12:24:43 +09:00
parent d8708caca5
commit f571462b95
11 changed files with 95 additions and 31 deletions

View File

@ -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>

View File

@ -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 .

View 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>
);
}

View File

@ -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"

View File

@ -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 }}>

View File

@ -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 }}>
아이디·비밀번호 찾기

View File

@ -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"

View File

@ -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>

View File

@ -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',

View File

@ -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을 완전히 껐다
컬렉션에서 로그인부터 순서대로 다시 실행 어제의 실험을 오늘 그대로
재현할 있다는 , 이게 컬렉션의 힘이에요.

View File

@ -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;
}