[FEAT] 설정 페이지 개인정보·탈퇴 화면 구현#127
Conversation
- Figma 디자인 기반으로 개인정보/약관/회원탈퇴 공용 네비를 구현했습니다 - 각 서브페이지에 제목+구분선 뼈대를 연결했습니다 - 회원 탈퇴용 신규 라우트(/settings/withdrawal)를 추가했습니다
- 프로필/캘린더 연동/언어/태그 설정 폼을 구현했습니다 - 기본 제공 태그(과제, 업무, 운동, 일상)는 삭제 버튼을 숨기고, 사용자가 추가한 태그만 삭제 가능하도록 했습니다
- 스크롤 영역을 main에만 적용해 사이드바가 화면에 고정되도록 수정했습니다
- 태그 칩, 태그 추가, 연동 버튼, 로그아웃 버튼 등에서 공통으로 쓰이는 pill 스타일 버튼 컴포넌트를 추가했습니다 - 삭제 아이콘이 있는 태그는 버튼 중첩을 피하기 위해 내부에만 실제 버튼을 렌더링하도록 했습니다
- 개별 버튼들을 공통 TagButton 컴포넌트로 교체했습니다 - 구글 캘린더 연동 해제 시 확인 다이얼로그를, 태그 추가 시 입력 다이얼로그를 임시로 노출하도록 구현했습니다 - 변경사항 저장 버튼을 데이터 변경 여부에 따라 활성화하고, 저장 실패 시 안내 메시지를 노출하도록 구현했습니다
- 사이드바 하위 메뉴와 개인정보 폼의 h2 라벨·버튼 텍스트를 Settings 네임스페이스 번역 키로 교체했습니다 (사용자 추가 태그는 번역 대상에서 제외) - 기본 태그를 캐노니컬 키로 저장하고 화면에서만 번역하도록 해서, 로케일과 무관하게 기본/커스텀 태그를 정확히 구분하도록 했습니다 - 언어 토글이 next-intl 로케일 라우팅과 실제로 연동되어 URL이 전환되도록 했습니다 - 구글 캘린더 연동 박스와 태그 목록의 불필요한 줄바꿈·여백 문제를 수정했습니다
- 언어 토글 클릭 시 하이라이트만 이동하고 변경사항 저장을 활성화하도록 하고, 실제 로케일 전환은 저장 성공 시에만 수행하도록 수정했습니다 - 로그아웃 시 로그인 페이지 이동 의도를 콘솔 로그로 남겨뒀습니다 (로그인 라우트가 아직 없어 실제 이동은 보류)
- Figma 디자인대로 탈퇴 안내 문구, 유의사항 박스, 탈퇴하기 버튼을 구현했습니다 - 탈퇴 버튼 클릭 시 확인 팝업(임시 window.confirm)을 노출하고, 확인 시에만 탈퇴 처리하도록 했습니다 - 다른 설정 페이지와 동일하게 Settings.withdrawal 네임스페이스로 한/영 번역을 적용했습니다 - 디자인 시스템에 없던 경고 아이콘을 WarningGrayIcon으로 추가했습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough설정 프로필 편집, 설정 네비게이션·레이아웃, 정책 및 회원 탈퇴 화면이 추가되었습니다. 프로필은 언어·태그·캘린더·저장 상태를 관리하며, PillButton과 TagChip 디자인 시스템 컴포넌트 및 관련 번역·라우트가 함께 추가되었습니다. Changes설정 페이지 기능
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/timo-web/app/`[locale]/(main)/settings/_containers/SettingsProfileContainer.tsx:
- Around line 64-79: The temporary dialog text in SettingsProfileContainer is
hardcoded in Korean, so replace the strings used by handleConnectCalendar and
the related dialog/toast helpers with translation keys via useTranslations. Add
the needed keys to the locale files, then read them in SettingsProfileContainer
instead of passing literal Korean text to window.confirm, window.prompt, and
window.alert so the same flow works for English users too.
In `@apps/timo-web/app/`[locale]/(main)/settings/policy/page.tsx:
- Around line 1-8: The SettingsPolicyPage component is hardcoding the page title
as Korean text, so replace that literal in SettingsPolicyPage with a translated
value from next-intl instead of rendering "약관" directly. Use useTranslations in
this Server Component to read the existing policy/nav translation key (for
example the same settings namespace used by tSettings("nav.policy")) and render
that value in the h1 so the locale switches correctly for all languages.
In
`@apps/timo-web/app/`[locale]/(main)/settings/withdrawal/_containers/SettingsWithdrawalContainer.tsx:
- Around line 25-32: Replace the temporary withdrawal flow in
SettingsWithdrawalContainer’s handleWithdraw with the real implementation
instead of the window.confirm placeholder and console.log stub. Hook the handler
up to the actual confirmation modal UI when available, and then call the
withdrawal API service for member deletion/data removal using the existing
handleWithdraw entry point so the TODOs are fully removed.
In `@packages/timo-design-system/src/components/button/tag-button/TagButton.tsx`:
- Around line 13-21: `TagButtonProps` currently allows `onRemove` without a
guaranteed accessible label, so update the `TagButton` types to use a
discriminated union where the branch with `onRemove` requires `removeLabel` and
the branch without `onRemove` disallows it or keeps it optional as appropriate.
Then adjust `TagButton` to rely on that type contract when setting the remove
button’s `aria-label`, using the `TagButton`/`TagButtonProps` symbols to find
the relevant props and render logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2fd93976-c4cd-4223-9bca-38ec73cfddc5
⛔ Files ignored due to path filters (3)
apps/timo-web/public/images/google-calendar.pngis excluded by!**/*.pngapps/timo-web/public/images/google-logo.pngis excluded by!**/*.pngpackages/timo-design-system/src/icons/source/warning-gray.svgis excluded by!**/*.svg
📒 Files selected for processing (19)
apps/timo-web/app/[locale]/(main)/settings/_components/SettingsProfileForm.tsxapps/timo-web/app/[locale]/(main)/settings/_containers/SettingsNavContainer.tsxapps/timo-web/app/[locale]/(main)/settings/_containers/SettingsProfileContainer.tsxapps/timo-web/app/[locale]/(main)/settings/_mocks/profile-mock.tsapps/timo-web/app/[locale]/(main)/settings/_types/profile-type.tsapps/timo-web/app/[locale]/(main)/settings/layout.tsxapps/timo-web/app/[locale]/(main)/settings/page.tsxapps/timo-web/app/[locale]/(main)/settings/policy/page.tsxapps/timo-web/app/[locale]/(main)/settings/withdrawal/_components/SettingsWithdrawalView.tsxapps/timo-web/app/[locale]/(main)/settings/withdrawal/_containers/SettingsWithdrawalContainer.tsxapps/timo-web/app/[locale]/(main)/settings/withdrawal/_queries/.gitkeepapps/timo-web/app/[locale]/(main)/settings/withdrawal/_types/withdrawal-type.tsapps/timo-web/app/[locale]/(main)/settings/withdrawal/page.tsxapps/timo-web/constants/routes.tsapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.jsonpackages/timo-design-system/src/components/button/tag-button/TagButton.stories.tsxpackages/timo-design-system/src/components/button/tag-button/TagButton.tsxpackages/timo-design-system/src/components/index.ts
| const handleConnectCalendar = () => { | ||
| if (profile.isCalendarConnected) { | ||
| // TODO: 실제 확인 모달로 교체 | ||
| const confirmed = window.confirm("구글 캘린더 연동을 해제하시겠습니까?"); | ||
| if (!confirmed) return; | ||
|
|
||
| // TODO: API - 연동 토큰 파기 | ||
| console.log("구글 캘린더 연동 토큰을 파기합니다."); | ||
| setProfile((prev) => ({ ...prev, isCalendarConnected: false })); | ||
| return; | ||
| } | ||
|
|
||
| // TODO: Google 계정 인증 및 캘린더 접근 권한 동의 팝업 호출 | ||
| console.log("Google Calendar 연동 인증 팝업을 호출합니다."); | ||
| setProfile((prev) => ({ ...prev, isCalendarConnected: true })); | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
임시 다이얼로그의 하드코딩된 한국어 문자열 i18n 처리 필요
window.confirm, window.prompt, window.alert에 하드코딩된 한국어 문자열이 사용되고 있습니다. PR 목표에서 임시 다이얼로그임을 명시하셨지만, 영어 사용자에게도 한국어가 표시되는 문제가 있습니다. 번역 키를 추가하고 useTranslations로 교체해주세요.
🔧 제안 수정안
const handleConnectCalendar = () => {
if (profile.isCalendarConnected) {
- const confirmed = window.confirm("구글 캘린더 연동을 해제하시겠습니까?");
+ const confirmed = window.confirm(tSettings("profile.confirmDisconnect"));
if (!confirmed) return; const handleAddTag = () => {
- const newTag = window.prompt("추가할 태그를 입력해주세요")?.trim();
+ const newTag = window.prompt(tSettings("profile.promptAddTag"))?.trim(); } catch {
- window.alert("저장에 실패했습니다. 잠시 후 다시 시도해 주세요.");
+ window.alert(tSettings("profile.saveFailed"));
} finally {Also applies to: 86-93, 110-128
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/timo-web/app/`[locale]/(main)/settings/_containers/SettingsProfileContainer.tsx
around lines 64 - 79, The temporary dialog text in SettingsProfileContainer is
hardcoded in Korean, so replace the strings used by handleConnectCalendar and
the related dialog/toast helpers with translation keys via useTranslations. Add
the needed keys to the locale files, then read them in SettingsProfileContainer
instead of passing literal Korean text to window.confirm, window.prompt, and
window.alert so the same flow works for English users too.
| export default function SettingsPolicyPage() { | ||
| return <></>; | ||
| return ( | ||
| <div className="flex flex-col gap-5 px-15 pt-7.5"> | ||
| <h1 className="typo-headline-m-16 text-timo-gray-900">약관</h1> | ||
| <hr className="border-timo-gray-500" /> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
하드코딩된 "약관" 텍스트를 i18n 번역 키로 교체 필요
네비게이션에서는 tSettings("nav.policy")를 사용해 "약관"/"Terms & Policies"로 다국어 처리하고 있지만, 정책 페이지 본문은 하드코딩된 한국어 "약관"을 사용하고 있습니다. 영어 locale에서도 한국어가 표시되는 i18n 결함입니다. next-intl v4는 Server Component에서 useTranslations를 지원하므로 바로 적용 가능합니다.
🔧 제안 수정안
+import { useTranslations } from "next-intl";
+
export default function SettingsPolicyPage() {
+ const t = useTranslations("Settings");
return (
<div className="flex flex-col gap-5 px-15 pt-7.5">
- <h1 className="typo-headline-m-16 text-timo-gray-900">약관</h1>
+ <h1 className="typo-headline-m-16 text-timo-gray-900">{t("nav.policy")}</h1>
<hr className="border-timo-gray-500" />
</div>
);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export default function SettingsPolicyPage() { | |
| return <></>; | |
| return ( | |
| <div className="flex flex-col gap-5 px-15 pt-7.5"> | |
| <h1 className="typo-headline-m-16 text-timo-gray-900">약관</h1> | |
| <hr className="border-timo-gray-500" /> | |
| </div> | |
| ); | |
| } | |
| import { useTranslations } from "next-intl"; | |
| export default function SettingsPolicyPage() { | |
| const t = useTranslations("Settings"); | |
| return ( | |
| <div className="flex flex-col gap-5 px-15 pt-7.5"> | |
| <h1 className="typo-headline-m-16 text-timo-gray-900">{t("nav.policy")}</h1> | |
| <hr className="border-timo-gray-500" /> | |
| </div> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/app/`[locale]/(main)/settings/policy/page.tsx around lines 1 -
8, The SettingsPolicyPage component is hardcoding the page title as Korean text,
so replace that literal in SettingsPolicyPage with a translated value from
next-intl instead of rendering "약관" directly. Use useTranslations in this Server
Component to read the existing policy/nav translation key (for example the same
settings namespace used by tSettings("nav.policy")) and render that value in the
h1 so the locale switches correctly for all languages.
| const handleWithdraw = () => { | ||
| // TODO: 실제 확인 모달로 교체 | ||
| const confirmed = window.confirm(t("confirmMessage")); | ||
| if (!confirmed) return; | ||
|
|
||
| // TODO: API - 회원 탈퇴 및 데이터 영구 삭제 | ||
| console.log("회원 탈퇴 API를 호출합니다."); | ||
| }; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial
TODO 항목 구현을 도울 수 있습니다.
window.confirm 임시 다이얼로그와 console.log 플레이스홀더에 대한 TODO가 명시되어 있습니다. PR 목표에서도 모달 UI가 준비되면 교체한다고 언급되어 있네요. 확인 모달 컴포넌트 구현이나 탈퇴 API 연동 로직 작성이 필요하시다면 도와드릴 수 있습니다. 새 이슈를 열어 추적하시겠습니까?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/timo-web/app/`[locale]/(main)/settings/withdrawal/_containers/SettingsWithdrawalContainer.tsx
around lines 25 - 32, Replace the temporary withdrawal flow in
SettingsWithdrawalContainer’s handleWithdraw with the real implementation
instead of the window.confirm placeholder and console.log stub. Hook the handler
up to the actual confirmation modal UI when available, and then call the
withdrawal API service for member deletion/data removal using the existing
handleWithdraw entry point so the TODOs are fully removed.
| export interface SettingsProfile { | ||
| name: string; | ||
| googleEmail: string; | ||
| isCalendarConnected: boolean; | ||
| tags: string[]; | ||
| } | ||
|
|
||
| export interface SettingsTagItem { | ||
| id: string; | ||
| label: string; | ||
| isDefault: boolean; | ||
| } | ||
|
|
||
| export interface SettingsProfileLabels { | ||
| title: string; | ||
| profileSection: string; | ||
| calendarSection: string; | ||
| connect: string; | ||
| disconnect: string; | ||
| languageSection: string; | ||
| languageKorean: string; | ||
| languageEnglish: string; | ||
| tagsSection: string; | ||
| addTag: string; | ||
| logout: string; | ||
| save: string; | ||
| removeTag: (tag: string) => string; | ||
| } |
There was a problem hiding this comment.
API 명세와 직접 연관되는 타입들은 zod 사용해서 런타임 타입 검증을 고려하면 좋을 것 같아요
There was a problem hiding this comment.
동기화 되어야하는 값이 많아서 확실히 zod 공부해보고 고려햐봐야할 것 같네요 😢
- 설정 프로필 폼 리팩터링을 위해 react-hook-form 패키지를 추가했습니다
- 컨테이너에서 직접 관리하던 draftLanguage·isDirty·isSaving useState를 제거하고 React Hook Form의 formState로 대체했습니다 - language·tags 변경을 setValue로 처리하고 저장 로직을 handleSubmit으로 감싸 컨테이너의 책임을 폼 상태 조율로 좁혔습니다
ehye1
left a comment
There was a problem hiding this comment.
설정 페이지 깔끔하게 빠르게 잘 만들어주셨네요!!
나중에 모달이랑 태그 입력 조건 등등 추가해주시면 되겟당
수고하셨습니다!!🤗😙
- 어떤 소비처에서도 TagButtonVariant를 import하지 않아 불필요한 export를 제거했습니다
- (main) 라우트 그룹에 이미 main 랜드마크가 있어 중복되는 settings 레이아웃의 main을 section으로 변경했습니다
- 라우팅 훅과 상태 선언 사이에 빈 줄을 추가해 가독성을 정리했습니다
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/timo-web/app/[locale]/(main)/settings/_types/profile-type.ts (1)
1-8: 📐 Maintainability & Code Quality | 🔵 Trivialtype alias에
Types접미사 누락
SettingsLanguage와SettingsDefaultTagKey는 유니언 타입 alias이므로 컨벤션에 따라Types접미사가 필요합니다. As per coding guidelines, "type alias는 유니언·튜플·리터럴에만 사용, 접미사Types"를 따라야 합니다.♻️ 제안 수정안
-export type SettingsLanguage = "ko" | "en"; +export type SettingsLanguageTypes = "ko" | "en"; -export type SettingsDefaultTagKey = +export type SettingsDefaultTagKeyTypes = | "assignment" | "work" | "exercise" | "dailyLife";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/timo-web/app/`[locale]/(main)/settings/_types/profile-type.ts around lines 1 - 8, Rename the union type aliases SettingsLanguage and SettingsDefaultTagKey to SettingsLanguageTypes and SettingsDefaultTagKeyTypes, and update all references throughout the codebase to use the new names.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@apps/timo-web/app/`[locale]/(main)/settings/_types/profile-type.ts:
- Around line 1-8: Rename the union type aliases SettingsLanguage and
SettingsDefaultTagKey to SettingsLanguageTypes and SettingsDefaultTagKeyTypes,
and update all references throughout the codebase to use the new names.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1facf3cf-b845-4890-8cad-2f898042bdcb
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
apps/timo-web/app/[locale]/(main)/settings/_containers/SettingsProfileContainer.tsxapps/timo-web/app/[locale]/(main)/settings/_types/profile-type.tsapps/timo-web/app/[locale]/(main)/settings/layout.tsxapps/timo-web/package.jsonpackages/timo-design-system/src/components/index.ts
💤 Files with no reviewable changes (1)
- packages/timo-design-system/src/components/index.ts
- TagButton이 태그 표시(정적·삭제가능)와 일반 클릭 액션(캘린더 연동·로그아웃 등)을 한 컴포넌트로 처리해 이름과 책임이 맞지 않아 분리했습니다 - TagChip은 태그 표시 전용으로 onRemove 유무만 분기하도록 단순화하고, PillButton은 onClick이 필수인 클릭 가능한 작은 버튼으로 정리했습니다
- TagButton 분리에 맞춰 태그 목록은 TagChip으로, 캘린더 연동·태그 추가·로그아웃 버튼은 PillButton으로 교체했습니다
- flex-col 부모의 기본 stretch로 버튼이 전체 너비를 차지해 Figma의 Hug 크기(111x44)와 어긋나 self-start를 추가했습니다
…to feat/web/122-settings-page-view # Conflicts: # apps/timo-web/package.json # packages/timo-design-system/src/components/index.ts
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/timo-web/messages/ko.json (1)
62-62: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win한국어 띄어쓰기를 수정해 주세요.
더이상은더 이상으로 표기해야 합니다.수정 예시
- "guideTitle": "더이상 Timo를 이용하지 않으시나요?", + "guideTitle": "더 이상 Timo를 이용하지 않으시나요?",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/timo-web/messages/ko.json` at line 62, In the Korean translation entry "guideTitle", correct the spacing in the displayed text by changing "더이상" to "더 이상".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/timo-design-system/src/components/button/pill-button/PillButton.stories.tsx`:
- Around line 1-2: Update the imports in the PillButton Storybook file to use
the package’s configured absolute path aliases instead of the relative
"./PillButton" and "../../../icons" paths, preserving the existing symbols.
In
`@packages/timo-design-system/src/components/button/pill-button/PillButton.tsx`:
- Line 1: Update the cn import in PillButton.tsx to use the repository’s
configured absolute path alias instead of the relative "../../../lib" path,
following the existing TypeScript/React import convention.
- Around line 5-9: Rename the union type alias PillButtonVariant to
PillButtonVariantTypes and update all references, including the Record key in
PILL_BUTTON_VARIANT and any component props or exports using the original name.
In `@packages/timo-design-system/src/components/tag/tag-chip/TagChip.stories.tsx`:
- Line 1: TagChip.stories.tsx의 TagChip import를 상대 경로 "./TagChip"에서 프로젝트에 정의된 절대
경로 alias import로 변경하세요.
In `@packages/timo-design-system/src/components/tag/tag-chip/TagChip.tsx`:
- Around line 1-2: Update the imports in TagChip.tsx to use the repository’s
configured absolute package aliases instead of the relative paths
"../../../icons" and "../../../lib", preserving the existing DeleteIcon and cn
symbols.
- Around line 8-9: TagChip의 삭제 버튼이 접근성 이름 없이 렌더링되지 않도록 onRemove와 removeLabel의
타입을 함께 조정하세요. onRemove가 제공되는 경우 removeLabel을 필수로 요구하고, TagChip 내부 삭제 버튼의
aria-label에 해당 값을 전달하며 관련 타입 정의와 사용처도 업데이트하세요.
---
Outside diff comments:
In `@apps/timo-web/messages/ko.json`:
- Line 62: In the Korean translation entry "guideTitle", correct the spacing in
the displayed text by changing "더이상" to "더 이상".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: ebc52247-d46a-448c-9a52-df5fffb15caa
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (10)
apps/timo-web/app/[locale]/(main)/settings/_components/SettingsProfileForm.tsxapps/timo-web/app/[locale]/(main)/settings/withdrawal/_components/SettingsWithdrawalView.tsxapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.jsonapps/timo-web/package.jsonpackages/timo-design-system/src/components/button/pill-button/PillButton.stories.tsxpackages/timo-design-system/src/components/button/pill-button/PillButton.tsxpackages/timo-design-system/src/components/index.tspackages/timo-design-system/src/components/tag/tag-chip/TagChip.stories.tsxpackages/timo-design-system/src/components/tag/tag-chip/TagChip.tsx
| import { PillButton } from "./PillButton"; | ||
| import { PlusGrayIcon } from "../../../icons"; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Storybook import도 절대 경로로 통일해 주세요.
./PillButton와 ../../../icons는 TypeScript/React 파일의 상대 경로 import 규칙을 위반합니다. 패키지에서 사용하는 절대 경로 alias로 변경해 주세요.
As per path instructions: TypeScript/React 파일은 상대 경로보다 절대 경로 import를 사용해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/timo-design-system/src/components/button/pill-button/PillButton.stories.tsx`
around lines 1 - 2, Update the imports in the PillButton Storybook file to use
the package’s configured absolute path aliases instead of the relative
"./PillButton" and "../../../icons" paths, preserving the existing symbols.
Source: Path instructions
| @@ -0,0 +1,39 @@ | |||
| import { cn } from "../../../lib"; | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
상대 경로 import를 절대 경로로 변경해 주세요.
../../../lib는 저장소의 TypeScript/React import 규칙을 위반합니다. 패키지에서 사용하는 절대 경로 alias로 cn을 가져오면 디렉터리 구조 변경에도 더 안전합니다. 참고: https://www.typescriptlang.org/docs/handbook/module-resolution.html
As per path instructions: TypeScript/React 파일은 상대 경로보다 절대 경로 import를 사용해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/timo-design-system/src/components/button/pill-button/PillButton.tsx`
at line 1, Update the cn import in PillButton.tsx to use the repository’s
configured absolute path alias instead of the relative "../../../lib" path,
following the existing TypeScript/React import convention.
Source: Path instructions
| export type PillButtonVariant = "gray" | "blue"; | ||
|
|
||
| const PILL_BUTTON_VARIANT: Record<PillButtonVariant, string> = { | ||
| gray: "bg-timo-gray-300 text-timo-gray-900", | ||
| blue: "bg-timo-blue-300 text-white", |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
유니언 타입 alias 이름에 Types 접미사를 사용해 주세요.
PillButtonVariant는 유니언 타입 alias이므로 PillButtonVariantTypes로 이름을 변경하고 Record 선언도 함께 갱신해야 합니다.
As per path instructions: 유니언·튜플·리터럴을 제외한 타입 alias에는 Types 접미사 규칙을 적용하며, 이 프로젝트의 해당 타입도 일관된 이름을 사용해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/timo-design-system/src/components/button/pill-button/PillButton.tsx`
around lines 5 - 9, Rename the union type alias PillButtonVariant to
PillButtonVariantTypes and update all references, including the Record key in
PILL_BUTTON_VARIANT and any component props or exports using the original name.
Source: Path instructions
| @@ -0,0 +1,44 @@ | |||
| import { TagChip } from "./TagChip"; | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Storybook import를 절대 경로로 변경해 주세요.
./TagChip는 TypeScript/React 파일의 상대 경로 import 규칙을 위반합니다. 프로젝트의 절대 경로 alias를 사용해 주세요.
As per path instructions: TypeScript/React 파일은 상대 경로보다 절대 경로 import를 사용해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/timo-design-system/src/components/tag/tag-chip/TagChip.stories.tsx`
at line 1, TagChip.stories.tsx의 TagChip import를 상대 경로 "./TagChip"에서 프로젝트에 정의된 절대
경로 alias import로 변경하세요.
Source: Path instructions
| import { DeleteIcon } from "../../../icons"; | ||
| import { cn } from "../../../lib"; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
상대 경로 import를 절대 경로로 변경해 주세요.
../../../icons와 ../../../lib는 저장소의 TypeScript/React import 규칙을 위반합니다. 패키지 alias를 사용해 import 경로를 고정해 주세요.
As per path instructions: TypeScript/React 파일은 상대 경로보다 절대 경로 import를 사용해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/timo-design-system/src/components/tag/tag-chip/TagChip.tsx` around
lines 1 - 2, Update the imports in TagChip.tsx to use the repository’s
configured absolute package aliases instead of the relative paths
"../../../icons" and "../../../lib", preserving the existing DeleteIcon and cn
symbols.
Source: Path instructions
| onRemove?: () => void; | ||
| removeLabel?: string; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
삭제 버튼의 접근성 이름을 타입 수준에서 보장해 주세요.
onRemove가 있으면 아이콘 전용 버튼이 렌더링되지만, removeLabel은 선택 사항이라 aria-label={undefined}인 버튼이 만들어질 수 있습니다. 삭제 가능한 상태에서는 removeLabel을 필수로 만들거나, 항상 현지화된 기본 라벨을 제공해 주세요. 참고: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label
As per path instructions: 텍스트 없는 버튼에는 aria-label이 필수입니다.
Also applies to: 24-30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/timo-design-system/src/components/tag/tag-chip/TagChip.tsx` around
lines 8 - 9, TagChip의 삭제 버튼이 접근성 이름 없이 렌더링되지 않도록 onRemove와 removeLabel의 타입을 함께
조정하세요. onRemove가 제공되는 경우 removeLabel을 필수로 요구하고, TagChip 내부 삭제 버튼의 aria-label에 해당
값을 전달하며 관련 타입 정의와 사용처도 업데이트하세요.
Source: Path instructions
| const labels: SettingsProfileLabels = { | ||
| title: tSettings("profile.title"), | ||
| profileSection: tSettings("profile.profileSection"), | ||
| calendarSection: tSettings("profile.calendarSection"), | ||
| connect: tSettings("profile.connect"), | ||
| disconnect: tSettings("profile.disconnect"), | ||
| languageSection: tSettings("profile.languageSection"), | ||
| languageKorean: tSettings("profile.languageKorean"), | ||
| languageEnglish: tSettings("profile.languageEnglish"), | ||
| tagsSection: tSettings("profile.tagsSection"), | ||
| addTag: tSettings("profile.addTag"), | ||
| logout: tSettings("profile.logout"), | ||
| save: tSettings("profile.save"), | ||
| removeTag: (tag: string) => tSettings("profile.removeTag", { tag }), | ||
| }; |
There was a problem hiding this comment.
이 다국어 매핑값들 객체로 한번에 추출해서, 컨테이너에서는 구조 분해 할당으로 간단하게 로직을 처리할 수 있을 것 같아요!
yumin-kim2
left a comment
There was a problem hiding this comment.
전체적으로 컨테이너와 뷰 컴포넌트의 역할이 잘 나뉘어 있어서 읽기 좋았습니다. 특히 개인정보 설정에서 언어/태그 변경값을 react-hook-form으로 관리한 덕분에 변경 여부와 저장 상태를 직접 상태로 하나씩 관리하지 않아도 되어 코드가 깔끔해진 것 같아요! (덕분에 react-hook-form 공부도 했네요✨)
작은 코멘트 남겼는데 확인해주세요! 수고하셨씁니다아아
| const handleAddTag = () => { | ||
| // TODO: 실제 태그 추가 모달로 교체 | ||
| const newTag = window.prompt("추가할 태그를 입력해주세요")?.trim(); | ||
| if (!newTag || tags.includes(newTag)) return; |
There was a problem hiding this comment.
화면설계서에 "1~10자의 중복 없는 신규 태그명 입력 시 활성화 상태로 클릭 가능" 이라는 조건이 있는데 현재는 중복 검사만 있고 길이 제한 로직이 없는 것 같습니다 확인 부탁드릴게용🙇♀️
| export const SettingsWithdrawalContainer = () => { | ||
| const t = useTranslations("Settings.withdrawal"); | ||
|
|
||
| const labels: SettingsWithdrawalLabels = { |
There was a problem hiding this comment.
탈퇴 화면 문구를 전부 i18n으로 관리해서 한국어/영어 확장에 대응한 점이 좋네용 👍
| removeTag: (tag: string) => tSettings("profile.removeTag", { tag }), | ||
| }; | ||
|
|
||
| const handleConnectCalendar = () => { |
There was a problem hiding this comment.
handleConnectCalendar가 실제로는 연동과 연동 해제를 모두 처리하고 있어서, handleToggleCalendarConnection처럼 연동/해제를 모두 포함하는 동작이 드러나는 이름도 고려해볼 수 있을 것 같습니다! 아니면 함수명이 너무 길어져서 안 좋을까요?
ISSUE 🔗
close #122
What is this PR? 🔍
설정(Settings) 페이지의 개인정보 화면과 회원 탈퇴 화면을 Figma 디자인 기준으로 구현하고, 한/영 다국어를 적용했습니다.
배경
settings/page.tsx와settings/withdrawal/page.tsx는 헤더(h1)만 있고 실제 콘텐츠가 없는 뼈대 상태였습니다.window.confirm/prompt/alert로 임시 대체했습니다. 태그·버튼 등 반복되는 UI는 디자인 시스템 공용 컴포넌트로 분리했습니다.설정 사이드바
layout.tsx에서 스크롤 컨테이너를main에만 적용(overflow-hidden+overflow-y-auto)해 사이드바를 고정시켰습니다.SettingsNavContainer는useTranslations("Settings")로nav.account/nav.policy/nav.withdrawal키를 조회하고, 최상단 "설정" 타이틀은 기존Navigation.settings키를 재사용해 중복 정의를 피했습니다.개인정보 화면
SettingsProfileContainer가isDirty플래그로 변경 여부를 추적해 저장 버튼 활성/비활성을 결정합니다. 캘린더 연동 해제 시window.confirm으로 확인 팝업을 흉내 내고, 확인 시에만 상태를 되돌립니다. 언어 토글은 화면설계서 규칙("탭 전환 시 화면 언어는 즉시 바뀌지 않고 저장 버튼만 활성화")에 따라 클릭 시엔draftLanguagestate만 바꾸고, 저장 성공 시에만@/i18n/navigation의router.replace(pathname, { locale })로 실제 로케일을 전환합니다. 태그는 기본 태그(과제/업무/운동/일상)를 캐노니컬 키(assignment/work/exercise/dailyLife)로 저장하고 화면에서Common.tag.*로 번역해, 로케일이 바뀌어도 기본/커스텀 태그 구분이 깨지지 않도록 했습니다. 태그 추가는window.promptplaceholder에 10자 제한과 중복 검사를 추가했습니다.공용 버튼 컴포넌트
TagButton으로 먼저 묶었다가, 리뷰 과정에서TagChip(태그 표시)과PillButton(클릭 액션)으로 용도별로 다시 분리했습니다.TagButton이라는 이름과 달리 실제로는 태그 표시(정적 기본 태그/삭제 가능한 커스텀 태그)와 캘린더 연동, 태그 추가, 로그아웃 같은 일반 클릭 액션까지 한 컴포넌트가 함께 떠맡고 있어 이름이 가리키는 역할과 실제 책임이 어긋났어요.variant·icon·onClick·onRemove4개 prop을 모두 받다 보니, 소비처마다 실제로 쓰는 조합은 서로 겹치지 않는데도onRemove→onClick유무 순으로 분기하는 로직 하나에 섞여 있었습니다.그래서
TagChip(packages/timo-design-system/src/components/tag/tag-chip)은 태그 표시 전용으로onRemove유무만으로 분기하고,variant·icon·onClick을 제거해 항상 gray 배경만 렌더링하도록 했어요.PillButton(.../components/button/pill-button)은 반대로onClick을 필수 prop으로 바꿔 클릭 가능한 액션(캘린더 연동variant="blue", 태그 추가icon, 로그아웃)만 담당합니다.삭제 가능한 태그는
<button>안에<button>을 중첩할 수 없어TagChip바깥을<div>로, 삭제 아이콘만 내부<button>으로 렌더링하는 기존 구조는 그대로 유지했습니다.태그 추가 버튼을
TagChip이 아니라PillButton으로 분류한 이유는 "태그 추가" 버튼은 실제 태그를 표시하는 게 아니라 클릭 가능한 액션(모달 열기/prompt 띄우기)이기 때문이에요. TagChip은 이번에 onRemove 유무로만 분기하는 순수 표시용 컴포넌트로 단순화했고 (onClick, icon prop을 아예 없앴음), 클릭 가능한 것들(캘린더 연동, 태그 추가, 로그아웃)은 전부 PillButton으로 모았습니다.다국어(i18n) 지원
messages/ko.json·en.json의Settings네임스페이스 번역을 적용했습니다.next-intl로 번역되고 있었는데, 설정 하위 화면들만 한국어로 고정되어 있어/en로케일 진입 시 일관성이 깨졌습니다._components는 순수 UI 원칙에 따라 직접 번역 훅을 쓰지 않고,_containers가useTranslations로 만든labels객체를 prop으로 내려받습니다. 사용자가 직접 추가한 커스텀 태그(예: "안녕")는 번역 대상에서 제외했습니다.회원 탈퇴 화면
window.confirm으로 "정말 탈퇴하시겠어요?" 확인을 거치고, 확인 시에만 콘솔 로그로 탈퇴 처리 placeholder를 실행합니다. 디자인 시스템에 없던 경고 아이콘은 Figma에서 내려받아warning-gray.svg소스로 추가하고icons:generate로WarningGrayIcon을 생성했습니다(생성 산출물은 gitignore 대상이라 소스 SVG만 커밋에 포함됩니다).To Reviewers
window.confirm/prompt/alert로 임시 처리했습니다. 실제 모달 컴포넌트로 교체하는 작업은 후속 PR로 남겨뒀습니다.SettingsProfile.tags가 캐노니컬 키(영문)와 커스텀 원문(한국어)을 한 배열에 섞어서 담는 구조라, 실제 API 연동 시에는 백엔드가 태그를 어떤 형태로 내려주는지에 따라 이 부분을 다시 다듬어야 할 수 있습니다.Screenshot 📷
Common.tag/Settings.profile번역 적용Test Checklist ✔
pnpm check-types통과pnpm lint통과/ko,/en개인정보·탈퇴 화면 렌더링 및 텍스트 확인/en으로 전환되는 흐름 확인pnpm build— 미실행: CI에서 확인 예정