From 82d85f8777da3d2738d45a1f6175b1b6e0dd6bf5 Mon Sep 17 00:00:00 2001 From: jinlee Date: Mon, 30 Jun 2025 10:32:34 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=EC=8B=9C=20=EA=B8=B0=EC=A1=B4=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=ED=99=95=EC=9D=B8=20(#212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/user/update/route.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/app/api/user/update/route.ts b/app/api/user/update/route.ts index 4737da2..6c84aec 100644 --- a/app/api/user/update/route.ts +++ b/app/api/user/update/route.ts @@ -47,7 +47,12 @@ export async function PATCH(req: Request) { const data: any = {}; for (const [key, value] of Object.entries(body)) { - if (value !== undefined && key !== 'password' && key !== 'oldPinCode') { + if ( + value !== undefined && + key !== 'password' && + key !== 'oldPinCode' && + key !== 'oldPassword' + ) { data[key] = value; } } @@ -81,6 +86,25 @@ export async function PATCH(req: Request) { } if (body.password !== undefined) { + if (!body.oldPassword) { + return NextResponse.json( + { error: '기존 비밀번호를 입력해야 합니다.' }, + { status: 400 } + ); + } + + const isPasswordValid = await bcrypt.compare( + body.oldPassword, + existingUser.password + ); + + if (!isPasswordValid) { + return NextResponse.json( + { error: '기존 비밀번호가 일치하지 않습니다.' }, + { status: 400 } + ); + } + data.password = await hash(body.password, 10); } From 857ee96eac6651dcc895496d4170e58bca1fd1ae Mon Sep 17 00:00:00 2001 From: VarGun Date: Mon, 30 Jun 2025 10:55:13 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20qa=201=EC=B0=A8=20=EB=B0=98=EC=98=81?= =?UTF-8?q?=20(#211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[category-id]/_components/etf-table.tsx | 2 +- .../_components/etf-detail-container.tsx | 18 +++++---- .../_components/article-page-container.tsx | 2 +- .../_components/shorts-viewer-container.tsx | 2 +- app/(routes)/isa/_components/portfolio.tsx | 38 +++++++++---------- .../main/_components/main-page-container.tsx | 1 - .../main/_components/no-account-modal.tsx | 4 +- .../_components/edit-profile-container.tsx | 8 ---- .../_components/edit-phone-container.tsx | 13 +++---- .../pin/_components/edit-pin-container.tsx | 5 --- app/(routes)/mypage/edit-profile/utils.tsx | 24 +++++++----- lib/api/my-page.ts | 9 +++++ 12 files changed, 63 insertions(+), 63 deletions(-) diff --git a/app/(routes)/etf/category/[category-id]/_components/etf-table.tsx b/app/(routes)/etf/category/[category-id]/_components/etf-table.tsx index 2b684c3..c4e4fc2 100644 --- a/app/(routes)/etf/category/[category-id]/_components/etf-table.tsx +++ b/app/(routes)/etf/category/[category-id]/_components/etf-table.tsx @@ -21,7 +21,7 @@ export default function EtfTable({ data }: EtfTableProps) { onClick={() => router.push(`/etf/detail/${item.etfId}`)} >
-
{item.name}
+
{item.name}
{item.code}
diff --git a/app/(routes)/etf/detail/[etf-code]/_components/etf-detail-container.tsx b/app/(routes)/etf/detail/[etf-code]/_components/etf-detail-container.tsx index 1180c21..5e25ce3 100644 --- a/app/(routes)/etf/detail/[etf-code]/_components/etf-detail-container.tsx +++ b/app/(routes)/etf/detail/[etf-code]/_components/etf-detail-container.tsx @@ -120,7 +120,7 @@ export default function EtfDetailContainer({

{etfIntro.category}

-
+
{etfIntro.issueName} @@ -133,15 +133,17 @@ export default function EtfDetailContainer({ {formatComma(etfIntro.todayClose)} - 0 ? 'up' : 'down'}`} - /> + {parseFloat(etfIntro.flucRate) !== 0 && ( + 0 ? 'up' : 'down'}`} + /> + )} 0 ? 'text-hana-red' : 'text-blue'}`} + className={`text-xs leading-none ${parseFloat(etfIntro.flucRate) > 0 ? 'text-hana-red' : parseFloat(etfIntro.flucRate) === 0 ? 'text-gray-500' : 'text-blue'}`} > - {parseInt(etfIntro.flucRate) > 0 - ? `+${parseInt(etfIntro.flucRate)} %` - : `${parseInt(etfIntro.flucRate)} %`} + {parseFloat(etfIntro.flucRate) > 0 + ? `+${parseFloat(etfIntro.flucRate)} %` + : `${parseFloat(etfIntro.flucRate)} %`}
diff --git a/app/(routes)/guide/articles/[id]/_components/article-page-container.tsx b/app/(routes)/guide/articles/[id]/_components/article-page-container.tsx index cf1e2ae..a0d1005 100644 --- a/app/(routes)/guide/articles/[id]/_components/article-page-container.tsx +++ b/app/(routes)/guide/articles/[id]/_components/article-page-container.tsx @@ -112,7 +112,7 @@ export default function ArticlePageContainer({ elements.push(

{line.slice(2)}

diff --git a/app/(routes)/guide/shorts-viewer/[category]/_components/shorts-viewer-container.tsx b/app/(routes)/guide/shorts-viewer/[category]/_components/shorts-viewer-container.tsx index 8e6734b..059bf46 100644 --- a/app/(routes)/guide/shorts-viewer/[category]/_components/shorts-viewer-container.tsx +++ b/app/(routes)/guide/shorts-viewer/[category]/_components/shorts-viewer-container.tsx @@ -66,7 +66,7 @@ export const ShortsViewerContainer = ({ session }: Props) => { return (

{pageTitle}

-
+
{filteredVideo.map( ({ id, title, duration, views, videoUrl, category }) => (
-
- {/* 왼쪽 블록 */} -
- -
-

- 전문가 모델 기반 리밸런싱을 원하시나요? -

-
- 하나은행 AI 기반 ISA 포트폴리오 추천과 연동해보세요. -
-
-
+ {/*
*/} + {/* /!* 왼쪽 블록 *!/*/} + {/*
*/} + {/* */} + {/*
*/} + {/*

*/} + {/* 전문가 모델 기반 리밸런싱을 원하시나요?*/} + {/*

*/} + {/*
*/} + {/* 하나은행 AI 기반 ISA 포트폴리오 추천과 연동해보세요.*/} + {/*
*/} + {/*
*/} + {/*
*/} - {/* 오른쪽 버튼 */} - -
+ {/* /!* 오른쪽 버튼 *!/*/} + {/* */} + {/*
*/}
diff --git a/app/(routes)/main/_components/main-page-container.tsx b/app/(routes)/main/_components/main-page-container.tsx index 42e566a..5e7aa00 100644 --- a/app/(routes)/main/_components/main-page-container.tsx +++ b/app/(routes)/main/_components/main-page-container.tsx @@ -191,7 +191,6 @@ export default function MainPageContainer({ userName, savedTax }: Props) { }; const handleAgree = () => { - console.log('어그리 ~'); setShowModal(false); setIsAgree(true); setShowConfirmModal(true); diff --git a/app/(routes)/main/_components/no-account-modal.tsx b/app/(routes)/main/_components/no-account-modal.tsx index 67b370d..129f16e 100644 --- a/app/(routes)/main/_components/no-account-modal.tsx +++ b/app/(routes)/main/_components/no-account-modal.tsx @@ -22,7 +22,7 @@ export default function NoIsaModal({ onClose }: NoIsaModalProps) { /** 모달 종료 + 메인 이동 */ const handleClose = () => { onClose?.(); - router.replace('/main'); + router.replace('/isa'); }; return ( @@ -48,7 +48,7 @@ export default function NoIsaModal({ onClose }: NoIsaModalProps) { onClick={handleClose} className='mt-6 w-full cursor-pointer rounded-lg bg-hana-green py-2 font-semibold text-white transition hover:bg-hana-green/90' > - 메인화면으로 이동 + 연결하러 가기
diff --git a/app/(routes)/mypage/edit-profile/_components/edit-profile-container.tsx b/app/(routes)/mypage/edit-profile/_components/edit-profile-container.tsx index f01c318..b92ee31 100644 --- a/app/(routes)/mypage/edit-profile/_components/edit-profile-container.tsx +++ b/app/(routes)/mypage/edit-profile/_components/edit-profile-container.tsx @@ -51,14 +51,6 @@ export const EditProfileContainer = () => { ))}
-
-
{showLeaveModal && ( setShowLeaveModal(false)} /> diff --git a/app/(routes)/mypage/edit-profile/phone/_components/edit-phone-container.tsx b/app/(routes)/mypage/edit-profile/phone/_components/edit-phone-container.tsx index 32f11ad..baf3d15 100644 --- a/app/(routes)/mypage/edit-profile/phone/_components/edit-phone-container.tsx +++ b/app/(routes)/mypage/edit-profile/phone/_components/edit-phone-container.tsx @@ -78,14 +78,11 @@ export const EditPhoneContainer = () => { const raw = value.replace(/\D/g, '').slice(0, 3); setPhoneData((prev) => ({ ...prev, verificationCode: raw })); - if (raw.length === 3 && raw === sentCode) { - setValidationErrors((prev) => ({ ...prev, verificationCode: false })); - } else { - setValidationErrors((prev) => ({ - ...prev, - verificationCode: !validateField('verificationCode', raw, phoneData), - })); - } + const isValid = raw.length === 3 && raw === sentCode; + setValidationErrors((prev) => ({ + ...prev, + verificationCode: !isValid, + })); return; } }; diff --git a/app/(routes)/mypage/edit-profile/pin/_components/edit-pin-container.tsx b/app/(routes)/mypage/edit-profile/pin/_components/edit-pin-container.tsx index 14636b8..ed5d5f5 100644 --- a/app/(routes)/mypage/edit-profile/pin/_components/edit-pin-container.tsx +++ b/app/(routes)/mypage/edit-profile/pin/_components/edit-pin-container.tsx @@ -45,10 +45,6 @@ export const EditPinContainer = () => { newPin: true, }); useEffect(() => { - console.log('validationErrors : ', validationErrors); - }, [validationErrors]); - useEffect(() => { - console.log('pinData : ', pinData); setValidationErrors({ oldPin: pinData.oldPin.length !== 6, newPin: pinData.newPin.length !== 6, @@ -59,7 +55,6 @@ export const EditPinContainer = () => { const raw = value.replace(/\D/g, '').slice(0, 6); setPinData((prev) => ({ ...prev, [field]: raw })); - console.log('gmlgml'); setValidationErrors((prev) => ({ ...prev, diff --git a/app/(routes)/mypage/edit-profile/utils.tsx b/app/(routes)/mypage/edit-profile/utils.tsx index 1a0a726..47cbacf 100644 --- a/app/(routes)/mypage/edit-profile/utils.tsx +++ b/app/(routes)/mypage/edit-profile/utils.tsx @@ -11,6 +11,7 @@ export const submitUserUpdate = async ({ onSuccess?: () => void; onFinally?: () => void; }) => { + const isPinUpdate = 'oldPinCode' in data && 'pinCode' in data; const res = await updateUser(data); if (res.success) { @@ -25,15 +26,20 @@ export const submitUserUpdate = async ({ onSuccess?.(); } else { - toast.error('잠시 후 다시 시도해주세요.', { - duration: 2000, - icon: , - style: { - borderRadius: '8px', - color: 'black', - fontWeight: '500', - }, - }); + toast.error( + isPinUpdate + ? '이전 비밀번호가 일치하지 않습니다.' + : '잠시 후 다시 시도해주세요.', + { + duration: 2000, + icon: , + style: { + borderRadius: '8px', + color: 'black', + fontWeight: '500', + }, + } + ); } onFinally?.(); diff --git a/lib/api/my-page.ts b/lib/api/my-page.ts index 3d6f27d..56deaa6 100644 --- a/lib/api/my-page.ts +++ b/lib/api/my-page.ts @@ -112,3 +112,12 @@ export const verifyPin = async (pin: string) => { return res.json(); }; + +export const verifyPassword = async (password: string) => { + const data = { password }; + const res = await fetch('/api/user/비번-인증', { method: 'GET' }); + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.message || '수정 에러'); + } +}; From 6cc1ae8a842691dbcd1fa07880e8f99a07ac9334 Mon Sep 17 00:00:00 2001 From: VarGun Date: Mon, 30 Jun 2025 11:07:27 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EC=A1=B4=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=ED=99=95=EC=9D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../register/_components/register-form.tsx | 1 + .../_components/edit-password-container.tsx | 34 +++++++++++++++++-- app/(routes)/mypage/edit-profile/utils.tsx | 5 ++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/app/(routes)/(auth)/register/_components/register-form.tsx b/app/(routes)/(auth)/register/_components/register-form.tsx index d6d1361..f8684e5 100644 --- a/app/(routes)/(auth)/register/_components/register-form.tsx +++ b/app/(routes)/(auth)/register/_components/register-form.tsx @@ -34,6 +34,7 @@ export interface FormData { password: string; passwordConfirm: string; pinCode: string; + oldPassword?: string; } interface ValidationErrors { diff --git a/app/(routes)/mypage/edit-profile/password/_components/edit-password-container.tsx b/app/(routes)/mypage/edit-profile/password/_components/edit-password-container.tsx index e66e48c..038005d 100644 --- a/app/(routes)/mypage/edit-profile/password/_components/edit-password-container.tsx +++ b/app/(routes)/mypage/edit-profile/password/_components/edit-password-container.tsx @@ -11,11 +11,13 @@ import { Label } from '@/components/ui/label'; import { validateField } from '@/lib/utils'; interface PasswordData { + oldPassword: string; password: string; passwordConfirm: string; } interface ValidationErrors { + oldPassword: string; password: boolean; passwordConfirm: boolean; } @@ -28,10 +30,12 @@ export const EditPasswordContainer = () => { }, []); const [passwordData, setPasswordData] = useState({ + oldPassword: '', password: '', passwordConfirm: '', }); const [validationErrors, setValidationErrors] = useState({ + oldPassword: '', password: true, passwordConfirm: true, }); @@ -41,6 +45,17 @@ export const EditPasswordContainer = () => { const [loading, setLoading] = useState(false); const handleInputChange = (field: keyof FormData, value: string) => { + if (field === 'oldPassword') { + setPasswordData((prev) => ({ ...prev, [field]: value })); + + const isValid = validateField(field, value, passwordData); + setValidationErrors((prev) => ({ + ...prev, + oldPassword: isValid ? '' : '비밀번호를 입력해주세요.', + })); + setShowPasswordError(!isValid && value.length > 0); + return; + } if (field === 'password') { const isValid = validateField(field, value, passwordData); setShowPasswordError(!isValid && value.length > 0); @@ -58,6 +73,7 @@ export const EditPasswordContainer = () => { const submitData = async () => { const data = { + oldPassword: passwordData.oldPassword, password: passwordData.password, }; setLoading(true); @@ -74,7 +90,21 @@ export const EditPasswordContainer = () => {
+ +
+
+ {
void; }) => { const isPinUpdate = 'oldPinCode' in data && 'pinCode' in data; + const isPasswordUpdate = 'oldPassword' in data && 'password' in data; const res = await updateUser(data); if (res.success) { @@ -29,7 +30,9 @@ export const submitUserUpdate = async ({ toast.error( isPinUpdate ? '이전 비밀번호가 일치하지 않습니다.' - : '잠시 후 다시 시도해주세요.', + : isPasswordUpdate + ? '기존 비밀번호를 입력해야 합니다.' + : '잠시 후 다시 시도해주세요.', { duration: 2000, icon: , From 624468474a159ce966a7f27816c821570b938d80 Mon Sep 17 00:00:00 2001 From: VarGun Date: Mon, 30 Jun 2025 11:32:53 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=EC=88=8F=EC=B8=A0=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EC=9E=AC=EC=83=9D=20=EC=88=98=EC=A0=95=20(#211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shorts-viewer/[category]/[id]/page.tsx | 2 + .../_components/shorts-scroll-viewer.tsx | 59 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/app/(routes)/guide/shorts-viewer/[category]/[id]/page.tsx b/app/(routes)/guide/shorts-viewer/[category]/[id]/page.tsx index e2363e8..524a3ca 100644 --- a/app/(routes)/guide/shorts-viewer/[category]/[id]/page.tsx +++ b/app/(routes)/guide/shorts-viewer/[category]/[id]/page.tsx @@ -16,6 +16,8 @@ export default async function Page({ params }: { params: Params }) { if (initialIndex === -1) notFound(); + console.log('filteredVideos : '); + return ( ); diff --git a/app/(routes)/guide/shorts-viewer/[category]/_components/shorts-scroll-viewer.tsx b/app/(routes)/guide/shorts-viewer/[category]/_components/shorts-scroll-viewer.tsx index f6f59d1..483e7a5 100644 --- a/app/(routes)/guide/shorts-viewer/[category]/_components/shorts-scroll-viewer.tsx +++ b/app/(routes)/guide/shorts-viewer/[category]/_components/shorts-scroll-viewer.tsx @@ -111,6 +111,61 @@ const ShortsScrollViewer: React.FC = ({ }; }, [filteredVideo]); + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + const iframe = entry.target.querySelector( + 'iframe' + ) as HTMLIFrameElement; + if (!iframe?.contentWindow) return; + + iframe.contentWindow.postMessage( + JSON.stringify({ + event: 'command', + func: entry.isIntersecting ? 'playVideo' : 'pauseVideo', + args: [], + }), + '*' + ); + + if (!entry.isIntersecting) { + iframe.contentWindow.postMessage( + JSON.stringify({ + event: 'command', + func: 'seekTo', + args: [0, true], + }), + '*' + ); + } + }); + }, + { + root: containerRef.current, + threshold: 0.9, + } + ); + const items = containerRef.current?.querySelectorAll('.video-container'); + items?.forEach((el) => observer.observe(el)); + + items?.forEach((el) => { + const iframe = el.querySelector('iframe') as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage( + JSON.stringify({ + event: 'command', + func: 'pauseVideo', + args: [], + }), + '*' + ); + } + }); + + return () => observer.disconnect(); + }, [filteredVideo]); + return (
= ({ {filteredVideo.map((video, idx) => (
{/*//
*/} {/*
*/} @@ -129,7 +184,7 @@ const ShortsScrollViewer: React.FC = ({