Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 79 additions & 36 deletions src/app/(protected)/mypage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
UpdateUserRequest,
UploadProfileImageResponse,
} from "@/features/users/types";
import { getPwConfirmError, getPwError, PasswordToggle } from "@/components/form/PasswordInput";
import { useQueryClient } from "@tanstack/react-query";

export default function AccountPage() {
Expand All @@ -31,6 +32,11 @@ export default function AccountPage() {
const [newPw, setNewPw] = useState("");
const [confirmPw, setConfirmPw] = useState("");
const [error, setError] = useState<string | undefined>(undefined);
const [confirmError, setConfirmError] = useState<string | undefined>(undefined);

const [showCur, setShowCur] = useState(false);
const [showNew, setShowNew] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);

const [saving, setSaving] = useState(false);
const [changing, setChanging] = useState(false);
Expand Down Expand Up @@ -64,9 +70,8 @@ export default function AccountPage() {
};

// 블러 시 비밀번호 검사
const onBlurConfirm = () => {
setError(!confirmPw || confirmPw === newPw ? undefined : "비밀번호가 일치하지 않습니다.");
};
const onBlurNew = () => setError(getPwError(newPw));
const onBlurConfirm = () => setConfirmError(getPwConfirmError(newPw, confirmPw));

// 프로필 저장 버튼 활성화 조건
const canSaveProfile = useMemo(() => {
Expand All @@ -78,8 +83,8 @@ export default function AccountPage() {

// 비밀번호 변경 버튼 활성화 조건
const canChange = useMemo(
() => curPw.length > 0 && newPw.length > 0 && confirmPw.length > 0 && !error,
[curPw, newPw, confirmPw, error],
() => curPw.length > 0 && newPw.length > 0 && confirmPw.length > 0 && !error && !confirmError,
[curPw, newPw, confirmPw, error, confirmError],
);

// 프로필 저장 로직
Expand Down Expand Up @@ -130,6 +135,7 @@ export default function AccountPage() {
setNewPw("");
setConfirmPw("");
setError(undefined);
setConfirmError(undefined);
} catch (e) {
const msg = e instanceof Error ? e.message : "비밀번호 변경 실패";
if (msg.includes("현재 비밀번호")) {
Expand Down Expand Up @@ -229,49 +235,86 @@ export default function AccountPage() {
label="현재 비밀번호"
className="[&>label]:text-brand-gray-700 [&>label]:text-lg"
>
<Input
type="password"
name="current-password"
autoComplete="current-password"
value={curPw}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setCurPw(e.target.value)}
placeholder="비밀번호 입력"
/>
<div className="relative">
<Input
id="currentPw"
type={showCur ? "text" : "password"}
name="current-password"
autoComplete="current-password"
value={curPw}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setCurPw(e.target.value)}
placeholder="비밀번호 입력"
rightIcon={
<PasswordToggle
show={showCur}
onToggle={() => setShowCur((s) => !s)}
controlsId="currentPw"
/>
}
/>
</div>
</Field>
<Field
id="newPw"
label="새 비밀번호"
error={error}
className="[&>label]:text-brand-gray-700 [&>label]:text-lg"
>
<Input
type="password"
name="new-password"
autoComplete="new-password"
value={newPw}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewPw(e.target.value)}
placeholder="새 비밀번호 입력"
/>
<div className="relative">
<Input
id="newPw"
type={showNew ? "text" : "password"}
name="new-password"
autoComplete="new-password"
value={newPw}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setNewPw(e.target.value);
setError(getPwError(e.target.value));
}}
onBlur={onBlurNew}
aria-invalid={!!error}
className={error ? "border-red-500" : ""}
placeholder="새 비밀번호 입력"
rightIcon={
<PasswordToggle
show={showNew}
onToggle={() => setShowNew((s) => !s)}
controlsId="newPw"
/>
}
/>
</div>
</Field>
<Field
id="confirmPw"
label="새 비밀번호 확인"
error={error}
error={confirmError}
className="[&>label]:text-brand-gray-700 [&>label]:text-lg"
>
<Input
type="password"
name="new-password-confirm"
autoComplete="new-password-confirm"
value={confirmPw}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setConfirmPw(e.target.value);
if (error) setError(undefined);
}}
onBlur={onBlurConfirm}
aria-invalid={!!error}
className={error ? "border-red-500" : ""}
placeholder="새 비밀번호 입력"
/>
<div className="relative">
<Input
id="confirmPw"
type={showConfirm ? "text" : "password"}
name="new-password-confirm"
autoComplete="new-password-confirm"
value={confirmPw}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setConfirmPw(e.target.value);
if (confirmError) setConfirmError(undefined);
}}
onBlur={onBlurConfirm}
aria-invalid={!!confirmError}
className={confirmError ? "border-red-500" : ""}
placeholder="새 비밀번호 입력"
rightIcon={
<PasswordToggle
show={showConfirm}
onToggle={() => setShowConfirm((s) => !s)}
controlsId="confirmPw"
/>
}
/>
</div>
</Field>

<MyButton
Expand Down
37 changes: 15 additions & 22 deletions src/app/(public)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Field from "@/components/form/Field";
import Input from "@/components/form/Input";
import MyButton from "@/components/common/Button";
import { profileAvatar } from "@/features/users/profileAvatar";
import { getPwError, PasswordToggle } from "@/components/form/PasswordInput";

type Errors = { email?: string; password?: string };
const trueEmail = (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim());
Expand Down Expand Up @@ -39,25 +40,22 @@ export default function LoginPage() {

// 블러 시 비밀번호 검사
const validatePwOnBlur = () => {
setErrors((s) => ({
...s,
password:
!values.password || values.password.length >= 8 ? undefined : "8자 이상 작성해 주세요.",
}));
setErrors((s) => ({ ...s, password: getPwError(values.password) }));
};

const canSubmit = trueEmail(values.email) && values.password.length >= 8;
const disabledSubmit = !canSubmit || submitting;

const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (submitting) return;
setSubmitting(true);
validateEmailOnBlur();
validatePwOnBlur();
if (!canSubmit) return;

try {
// 로그인 성공
setSubmitting(true);

const res = await fetch("/api/login", {
method: "POST",
Expand All @@ -74,7 +72,6 @@ export default function LoginPage() {
} catch (e) {
console.warn("프로필 기본 이미지 적용 실패:", e);
}

router.replace("/mydashboard");
} catch (err: unknown) {
// 로그인 실패
Expand All @@ -83,6 +80,9 @@ export default function LoginPage() {
window.alert(message);
} finally {
setSubmitting(false);
// alert 닫힌 뒤 재제출 방지
(document.activeElement as HTMLElement | null)?.blur();
setValues((s) => ({ ...s, password: "" }));
}
};

Expand All @@ -108,29 +108,22 @@ export default function LoginPage() {
<Field id="password" label="비밀번호" error={errors.password}>
<div className="relative">
<Input
id="password"
aria-invalid={!!errors.password}
aria-errormessage="password-error"
type={showPw ? "text" : "password"}
placeholder="비밀번호를 입력해 주세요"
value={values.password}
onChange={onchange("password")}
onBlur={validatePwOnBlur}
rightIcon={
<PasswordToggle
show={showPw}
onToggle={() => setShowPw((v) => !v)}
controlsId="password"
/>
}
/>
<button
type="button"
onClick={() => setShowPw((s) => !s)}
aria-pressed={showPw}
aria-controls="password"
aria-label={showPw ? "비밀번호 숨기기" : "비밀번호 보기"}
className="absolute top-1/2 right-3 flex h-6 w-6 -translate-y-1/2 items-center justify-center text-gray-500"
>
<Image
src={showPw ? "/icons/icon-eye-close.svg" : "/icons/icon-eye-open.svg"}
alt="눈 아이콘"
width={24}
height={24}
/>
</button>
</div>
</Field>

Expand Down
Loading