Skip to content

feat: 프로필 이미지 추가#50

Open
dasosann wants to merge 2 commits intomainfrom
feat/onboarding-image
Open

feat: 프로필 이미지 추가#50
dasosann wants to merge 2 commits intomainfrom
feat/onboarding-image

Conversation

@dasosann
Copy link
Copy Markdown
Contributor

No description provided.

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

성별별 프로필 이미지 필터링 및 상태 관리 개선

✨ Enhancement

Grey Divider

Walkthroughs

Description
• 성별별 프로필 이미지 자동 필터링 및 선택 로직 추가
• Zustand 기반 상태 관리로 Context API 마이그레이션
• 프로필 빌더 단계별 선택 상태 추적 개선
• 프로필 이미지 UI/UX 개선 및 반응형 레이아웃 업데이트
Diagram
flowchart LR
  A["프로필 데이터"] -->|성별 정보| B["DefaultProfileAsset"]
  B -->|getDefaultProfilesByGender| C["성별별 필터링"]
  C -->|프로필 목록| D["DefaultProfileDrawer"]
  D -->|선택| E["ProfileImageSelection"]
  E -->|업데이트| F["Zustand Store"]
  F -->|persist| G["localStorage"]
Loading

Grey Divider

File Changes

1. app/profile-image/_constants/defaultProfiles.ts ✨ Enhancement +136/-0

성별별 프로필 이미지 필터링 로직 추가

app/profile-image/_constants/defaultProfiles.ts


2. app/globals.css ✨ Enhancement +10/-0

핑크 그래디언트 유틸리티 클래스 추가

app/globals.css


3. app/layout.tsx ✨ Enhancement +5/-8

ProfileProvider 제거 및 레이아웃 간소화

app/layout.tsx


View more (10)
4. app/profile-builder/_components/ScreenProfileBuilder.tsx ✨ Enhancement +23/-19

단계별 선택 상태 추적 로직 개선

app/profile-builder/_components/ScreenProfileBuilder.tsx


5. app/profile-builder/_components/Step3MBTI.tsx ✨ Enhancement +2/-3

MBTI 선택 실시간 동기화 개선

app/profile-builder/_components/Step3MBTI.tsx


6. app/profile-image/_components/DefaultProfileDrawer.tsx ✨ Enhancement +21/-26

성별별 프로필 필터링 및 UI 개선

app/profile-image/_components/DefaultProfileDrawer.tsx


7. app/profile-image/_components/ProfileBottomSheet.tsx ✨ Enhancement +3/-5

패딩 및 스크롤 레이아웃 조정

app/profile-image/_components/ProfileBottomSheet.tsx


8. app/profile-image/_components/ProfileImageSelection.tsx ✨ Enhancement +18/-8

성별별 프로필 선택 로직 및 이미지 렌더링 개선

app/profile-image/_components/ProfileImageSelection.tsx


9. app/profile-image/_components/ScreenProfileImage.tsx ✨ Enhancement +36/-3

성별 검증 및 기본 프로필 폴백 로직 추가

app/profile-image/_components/ScreenProfileImage.tsx


10. package.json Dependencies +2/-1

Zustand 상태 관리 라이브러리 추가

package.json


11. pnpm-lock.yaml Dependencies +26/-0

Zustand 의존성 잠금 파일 업데이트

pnpm-lock.yaml


12. providers/profile-provider.tsx ✨ Enhancement +48/-56

Context API에서 Zustand 스토어로 마이그레이션

providers/profile-provider.tsx


13. providers/service-status-provider.tsx ✨ Enhancement +20/-14

Context API에서 Zustand 스토어로 마이그레이션

providers/service-status-provider.tsx


Grey Divider

Qodo Logo


✨ Describe tool usage guide:

Overview:
The describe tool scans the PR code changes, and generates a description for the PR - title, type, summary, walkthrough and labels. The tool can be triggered automatically every time a new PR is opened, or can be invoked manually by commenting on a PR.

When commenting, to edit configurations related to the describe tool (pr_description section), use the following template:

/describe --pr_description.some_config1=... --pr_description.some_config2=...

With a configuration file, use the following template:

[pr_description]
some_config1=...
some_config2=...
Enabling\disabling automation
  • When you first install the app, the default mode for the describe tool is:
pr_commands = ["/describe", ...]

meaning the describe tool will run automatically on every PR.

  • Markers are an alternative way to control the generated description, to give maximal control to the user. If you set:
pr_commands = ["/describe --pr_description.use_description_markers=true", ...]

the tool will replace every marker of the form pr_agent:marker_name in the PR description with the relevant content, where marker_name is one of the following:

  • type: the PR type.
  • summary: the PR summary.
  • walkthrough: the PR walkthrough.
  • diagram: the PR sequence diagram (if enabled).

Note that when markers are enabled, if the original PR description does not contain any markers, the tool will not alter the description at all.

Custom labels

The default labels of the describe tool are quite generic: [Bug fix, Tests, Enhancement, Documentation, Other].

If you specify custom labels in the repo's labels page or via configuration file, you can get tailored labels for your use cases.
Examples for custom labels:

  • Main topic:performance - pr_agent:The main topic of this PR is performance
  • New endpoint - pr_agent:A new endpoint was added in this PR
  • SQL query - pr_agent:A new SQL query was added in this PR
  • Dockerfile changes - pr_agent:The PR contains changes in the Dockerfile
  • ...

The list above is eclectic, and aims to give an idea of different possibilities. Define custom labels that are relevant for your repo and use cases.
Note that Labels are not mutually exclusive, so you can add multiple label categories.
Make sure to provide proper title, and a detailed and well-phrased description for each label, so the tool will know when to suggest it.

Inline File Walkthrough 💎

For enhanced user experience, the describe tool can add file summaries directly to the "Files changed" tab in the PR page.
This will enable you to quickly understand the changes in each file, while reviewing the code changes (diffs).

To enable inline file summary, set pr_description.inline_file_summary in the configuration file, possible values are:

  • 'table': File changes walkthrough table will be displayed on the top of the "Files changed" tab, in addition to the "Conversation" tab.
  • true: A collapsable file comment with changes title and a changes summary for each file in the PR.
  • false (default): File changes walkthrough will be added only to the "Conversation" tab.
Utilizing extra instructions

The describe tool can be configured with extra instructions, to guide the model to a feedback tailored to the needs of your project.

Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter. Notice that the general structure of the description is fixed, and cannot be changed. Extra instructions can change the content or style of each sub-section of the PR description.

Examples for extra instructions:

[pr_description]
extra_instructions="""- The PR title should be in the format: '<PR type>: <title>'
- The title should be short and concise (up to 10 words)
- ...
"""

Use triple quotes to write multi-line instructions. Use bullet points to make the instructions more readable.

More PR-Agent commands

To invoke the PR-Agent, add a comment using one of the following commands:

  • /review: Request a review of your Pull Request.
  • /describe: Update the PR title and description based on the contents of the PR.
  • /improve [--extended]: Suggest code improvements. Extended mode provides a higher quality feedback.
  • /ask <QUESTION>: Ask a question about the PR.
  • /update_changelog: Update the changelog based on the PR's contents.
  • /help_docs <QUESTION>: Given a path to documentation (either for this repository or for a given one), ask a question.
  • /add_docs 💎: Generate docstring for new components introduced in the PR.
  • /generate_labels 💎: Generate labels for the PR based on the PR's contents.
  • /analyze 💎: Automatically analyzes the PR, and presents changes walkthrough for each component.

See the tools guide for more details.
To list the possible configuration parameters, add a /config comment.

See the describe usage page for a comprehensive guide on using this tool.

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 31, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Broken stored profile parsing🐞 Bug ≡ Correctness
Description
ScreenProfileBuilder parses localStorage("onboarding-profile-data") as if it were a ProfileData, but
profile-provider now persists an object keyed under "profile" via zustand persist. This makes
initialValues.* resolve to empty (e.g., parsed.birthDate/university undefined), so previously saved
onboarding fields won’t preload correctly.
Code

providers/profile-provider.tsx[R22-52]

+export const useProfileStore = create<ProfileStoreState>()(
+  persist(
+    (set, get) => ({
+      profile: {} as ProfileData,
+      isReady: false,
+      setReady: (ready) => set({ isReady: ready }),
+      updateProfile: (data) => {
+        const updated = { ...get().profile, ...data };
+        set({ profile: updated });
+      },
+      clearProfile: () => {
+        set({ profile: {} as ProfileData });
+        try {
+          LEGACY_STORAGE_KEYS.forEach((key) => localStorage.removeItem(key));
+        } catch (error) {
+          console.error("Failed to clear legacy profile storage:", error);
+        }
+      },
+    }),
+    {
+      name: STORAGE_KEY,
+      storage: createJSONStorage(() => localStorage),
+      partialize: (state) => ({ profile: state.profile }),
+      onRehydrateStorage: () => (state, error) => {
+        if (error) {
+          console.error("Failed to hydrate profile store:", error);
+        }
+        state?.setReady(true);
+      },
+    },
+  ),
Evidence
profile-provider now persists only a wrapped object containing a "profile" field, while
ScreenProfileBuilder JSON.parse’s the same key and reads ProfileData fields at the top level
(birthDate/university/etc.), which will be undefined when the stored object is wrapped.

providers/profile-provider.tsx[22-52]
app/profile-builder/_components/ScreenProfileBuilder.tsx[83-104]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ScreenProfileBuilder.getInitialValues()` reads `localStorage.getItem("onboarding-profile-data")` and casts it to `Partial<ProfileData>`, then accesses `birthDate`, `university`, etc. After this PR, the persisted value under that key is no longer a `ProfileData` shape (it’s persisted via zustand and partialized under a `profile` key), so those fields will not be found and the form won’t restore saved values.
### Issue Context
- `providers/profile-provider.tsx` uses zustand `persist(...)` with `name: STORAGE_KEY` and `partialize: (state) => ({ profile: state.profile })`.
- `ScreenProfileBuilder.tsx` still directly parses the storage key and reads `parsed.birthDate`, `parsed.university`, ...
### Fix Focus Areas
- app/profile-builder/_components/ScreenProfileBuilder.tsx[83-106]
- providers/profile-provider.tsx[22-52]
### What to change
- Stop parsing `onboarding-profile-data` as `ProfileData` in `ScreenProfileBuilder`.
- Preferred: use the hydrated zustand `profile` from `useProfile()` as the single source of truth.
- If you must read localStorage, parse the persisted structure and extract the nested `profile` object safely.
- Add defensive parsing so unexpected shapes don’t short-circuit the fallback-to-store path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Prefilled steps blocked🐞 Bug ≡ Correctness
Description
ScreenProfileBuilder now requires hasSelected* flags for step validity, but the initialization
effect only sets selected* values and never sets the corresponding hasSelected* flags. This disables
progression (Next button stays disabled) for users whose values are prefilled from saved state.
Code

app/profile-builder/_components/ScreenProfileBuilder.tsx[R212-222]

         selectedBirthYear &&
         selectedUniversity &&
         selectedDepartment &&
-          selectedMajor
+          selectedMajor &&
+          hasSelectedBirthYear &&
+          hasSelectedUniversity &&
+          hasSelectedDepartment &&
+          hasSelectedMajor
       );
     case 2:
       return !!selectedGender && hasSelectedGender;
Evidence
The initialization effect populates
selectedBirthYear/university/department/major/gender/mbti/frequency but doesn’t set the hasSelected*
booleans; validity checks for every step now require both the selected value and the hasSelected
flag, and those flags are only set by user-driven change handlers.

app/profile-builder/_components/ScreenProfileBuilder.tsx[131-156]
app/profile-builder/_components/ScreenProfileBuilder.tsx[193-206]
app/profile-builder/_components/ScreenProfileBuilder.tsx[208-229]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`isStepValid` now depends on `hasSelected*` flags, but when restoring values from storage (`useEffect` hydration), only the `selected*` states are set. This makes the UI treat restored fields as invalid until the user reselects every option.
### Issue Context
- Hydration effect sets `setSelectedX(...)` but never calls `setHasSelectedX(true)`.
- `isStepValid` requires e.g. `selectedGender && hasSelectedGender`, and step 1 requires all `hasSelectedBirthYear/University/Department/Major`.
### Fix Focus Areas
- app/profile-builder/_components/ScreenProfileBuilder.tsx[131-156]
- app/profile-builder/_components/ScreenProfileBuilder.tsx[208-229]
### What to change
- When you apply `initialValues.*` in the hydration effect, also set the matching `hasSelected*` flags to `true`.
- Alternatively, remove the `hasSelected*` checks from `isStepValid` and rely on presence/format validation only (if the intent isn’t to require re-confirmation).
- Ensure step 2/3/4 restored defaults don’t lock the user out of the Next/Complete actions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Maintenance state first-render wrong 🐞 Bug ☼ Reliability
Description
ServiceStatusProvider initializes the zustand store with isMaintenance=false and only syncs the
query result into the store in a useEffect, so consumers can observe false on the first render even
when initialMaintenanceMode/initialData is true. This can briefly render the wrong
maintenance/non-maintenance UI before the effect runs.
Code

providers/service-status-provider.tsx[R12-18]

+export const useServiceStatusStore = create<ServiceStatusStore>((set) => ({
+  isMaintenance: false,
+  setMaintenance: (value) => set({ isMaintenance: value }),
+}));

export function ServiceStatusProvider({
 children,
Evidence
The store default is false and is only updated after mount via useEffect; meanwhile the query has
initialData=initialMaintenanceMode, but that value is not applied to the store during initial
render.

providers/service-status-provider.tsx[12-15]
providers/service-status-provider.tsx[26-39]
providers/service-status-provider.tsx[41-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The maintenance flag in zustand is updated only after render, so any component reading `useServiceStatus()` during the first render may see `false` even if `initialMaintenanceMode` is `true`.
### Issue Context
- Store default: `isMaintenance: false`.
- Query uses `initialData: initialMaintenanceMode`.
- Store sync happens in `useEffect`, which runs after the first paint.
### Fix Focus Areas
- providers/service-status-provider.tsx[12-45]
### What to change
- Ensure the store is initialized/seeded with `initialMaintenanceMode` before children render.
- Example approach: set the store once on mount using `initialMaintenanceMode` (or use a layout effect), and then keep syncing future query updates.
- Or initialize the store value from the prop when creating/setting state for the first time in the provider.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Bottom sheet scroll clipped 🐞 Bug ☼ Reliability
Description
ProfileBottomSheet removed overflow-y-auto from the constrained max-h content container while the
outer wrapper uses overflow-hidden and the body is locked to overflow:hidden. If children exceed
60vh, the excess content can be clipped with no scrollable container available.
Code

app/profile-image/_components/ProfileBottomSheet.tsx[83]

+        <div className="scrollbar-hide max-h-[60vh]">{children}</div>
Evidence
When open, the component sets document.body.style.overflow='hidden' and the fixed root wrapper has
overflow-hidden; after removing overflow-y-auto on the content wrapper, there’s no element that can
scroll the overflowing content within the 60vh constraint.

app/profile-image/_components/ProfileBottomSheet.tsx[26-40]
app/profile-image/_components/ProfileBottomSheet.tsx[47-48]
app/profile-image/_components/ProfileBottomSheet.tsx[83-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The sheet content area is height-limited (`max-h-[60vh]`) but no longer scrollable. Because the body is also locked and the wrapper clips overflow, users may not be able to access all profile options or any content that extends beyond the limit.
### Issue Context
- Body scroll is disabled while the sheet is open.
- The wrapper uses `overflow-hidden`.
- The content container no longer sets `overflow-y-auto`.
### Fix Focus Areas
- app/profile-image/_components/ProfileBottomSheet.tsx[26-40]
- app/profile-image/_components/ProfileBottomSheet.tsx[47-48]
- app/profile-image/_components/ProfileBottomSheet.tsx[83-85]
### What to change
- Add `overflow-y-auto` (or `overflow-auto`) back to the constrained content container.
- If the footer should remain fixed while content scrolls, ensure only the content area scrolls and the header/footer remain outside that scrolling region.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이번 풀요청은 기존 React Context 기반의 profile 및 service-status 상태 관리를 Zustand로 마이그레이션하여 상태 관리 효율성과 데이터 지속성을 높였으며, 성별에 따른 맞춤형 기본 프로필 이미지 제공 로직을 새롭게 도입했습니다. 프로필 빌더의 유효성 검사 로직을 세분화하고 전반적인 UI 컴포넌트의 스타일 및 UX를 개선하는 작업이 포함되었습니다. 코드 리뷰 결과, ProfileBottomSheet.tsx 컴포넌트에서 최대 높이(max-h)가 제한된 영역의 overflow-y-auto 속성이 누락되어 콘텐츠가 넘칠 경우 사용자가 스크롤할 수 없는 잠재적인 UI 문제가 발견되었습니다.

@dasosann
Copy link
Copy Markdown
Contributor Author

dasosann commented Apr 1, 2026

@gemini-code-assist review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이번 PR은 프로필 및 서비스 상태 관리를 React Context에서 Zustand로 마이그레이션하고, 성별 맞춤형 기본 프로필 이미지 시스템과 새로운 약관 동의 바텀 시트를 도입했습니다. 코드 리뷰 결과, AgreeBottomSheet 컴포넌트 내 setTimeout 타이머들의 확실한 클린업 처리가 필요하며, useProfile 훅에서 useShallow를 사용하여 불필요한 리렌더링을 방지할 것을 권장합니다. 또한, Zustand의 persist 기능을 활용해 중복된 localStorage 직접 접근 로직을 정리하고, ProfileBottomSheet에서 제거된 스크롤 기능을 복구하여 UI 안정성을 높여야 합니다.

Comment on lines +26 to +42
useEffect(() => {
let timer: NodeJS.Timeout;
if (isOpen) {
// 부드러운 애니메이션을 위해 렌더링/애니메이션 상태를 다음 틱에서 설정
timer = setTimeout(() => {
setIsRendered(true);
setTimeout(() => setIsAnimating(true), 10);
}, 0);
document.body.style.overflow = "hidden";
} else {
timer = setTimeout(() => setIsAnimating(false), 0);
// transition duration(300ms) 후 언마운트
setTimeout(() => setIsRendered(false), 300);
document.body.style.overflow = "unset";
}
return () => clearTimeout(timer);
}, [isOpen]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

useEffect 내에서 생성된 여러 setTimeout 타이머들이 클린업 함수에서 모두 정리되지 않고 있습니다. 특히 시트가 닫힐 때 300ms 후에 실행되는 setIsRendered(false) 로직은 컴포넌트가 그 전에 언마운트되거나 isOpen 상태가 빠르게 변할 경우 레이스 컨디션(Race Condition)을 유발하여 UI가 비정상적으로 동작할 수 있습니다. 모든 타이머를 변수에 할당하고 return 블록에서 clearTimeout을 호출하여 확실히 정리해 주시기 바랍니다.

References
  1. State variables used for controlling UI animations or transitions are necessary and must be managed robustly, including proper cleanup of timers to prevent race conditions.

Comment on lines +55 to +62
export function useProfile() {
const profile = useProfileStore((state) => state.profile);
const updateProfile = useProfileStore((state) => state.updateProfile);
const clearProfile = useProfileStore((state) => state.clearProfile);
const isReady = useProfileStore((state) => state.isReady);

return { profile, updateProfile, clearProfile, isReady };
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

useProfile 훅이 호출될 때마다 새로운 객체 리터럴을 반환하고 있습니다. 이로 인해 Zustand 스토어의 상태가 변경될 때마다 이 훅을 사용하는 모든 컴포넌트가 불필요하게 리렌더링될 수 있습니다. 성능 최적화를 위해 useShallow를 사용하여 반환되는 객체의 얕은 비교를 수행하거나, 컴포넌트에서 필요한 상태만 개별적으로 선택(selector)하여 사용하도록 개선하는 것을 권장합니다. (참고: zustand/react/shallow에서 useShallow를 임포트해야 합니다.)

References
  1. React 19 및 Next.js 환경에서 불필요한 리렌더링을 방지하고 성능을 최적화하기 위해 Zustand의 useShallow 사용을 권장합니다. (link)
  2. Refactoring state management logic is appropriate when it addresses clear performance issues, such as preventing unnecessary re-renders in global stores.

Comment on lines 76 to 87
const getInitialValues = () => {
try {
const savedProfile = localStorage.getItem(PROFILE_STORAGE_KEY);
if (savedProfile) {
const parsed = JSON.parse(savedProfile) as Partial<ProfileData>;
const parsedRaw = JSON.parse(savedProfile);
// Zustand persist format matches: { state: { profile: ProfileData }, version: number }
const parsed = (parsedRaw.state?.profile ||
parsedRaw) as Partial<ProfileData>;

return {
birthYear: parsed.birthDate ? parsed.birthDate.split("-")[0] : "",
university: parsed.university || "",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

getInitialValues 함수에서 localStorage를 직접 조회하고 파싱하는 로직은 현재 도입된 Zustand의 persist 기능과 중복됩니다. Zustand 스토어가 이미 해당 키의 데이터를 관리하고 있으므로, isReady 상태를 확인한 후 스토어의 profile 객체를 직접 사용하여 로컬 상태를 초기화하는 것이 코드의 일관성과 유지보수 측면에서 더 좋습니다. 수동으로 localStorage를 파싱할 때 발생할 수 있는 에러 처리 부담도 줄일 수 있습니다.

References
  1. Refactoring state management is justified when it addresses significant maintenance risks or redundancy, such as consolidating state sources to a single managed store.

<div className="scrollbar-hide max-h-[60vh] overflow-y-auto">
{children}
</div>
<div className="max-h-[60vh]">{children}</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

overflow-y-auto 클래스가 제거되어, 콘텐츠가 max-h-[60vh]를 초과할 경우 스크롤이 되지 않고 내용이 잘릴 수 있습니다. 모바일 환경에서 다양한 콘텐츠 길이에 대응하기 위해 스크롤 기능을 유지하는 overflow-y-auto 클래스를 다시 추가하는 것을 권장합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant