Skip to content

[Refactor] experience-detail 구조 정리 및 내부 API 범위 축소#165

Merged
u-zzn merged 20 commits into
devfrom
refactor/#164/experience-detail-cleanup
Apr 8, 2026
Merged

[Refactor] experience-detail 구조 정리 및 내부 API 범위 축소#165
u-zzn merged 20 commits into
devfrom
refactor/#164/experience-detail-cleanup

Conversation

@u-zzn
Copy link
Copy Markdown
Collaborator

@u-zzn u-zzn commented Mar 29, 2026

✏️ Summary

다음 pr에서 experience-detail feature의 상태 관리 흐름 자체를 변경하는 구체적인 리팩토링 전, 크게 아래 4가지 내용의 사전 정리 작업 리팩토링 진행했습니다!

  1. 실제로 import / 호출되지 않는 dead code 제거
  2. index.ts export 범위 정리로 외부 공개 API 명확화
  3. ExperienceForm / ExperienceViewer의 공통 외부 레이아웃을 ExperienceLayout으로 추출
  4. alert 관련 구조를 단순화하고 내부 구현을 캡슐화

현재 experience-detail feature가 가지고 있는 “내부 구현 노출 범위”, “중복 구조”, “불필요한 우회 레이어”를 줄이고자 하였고, 아래 내용을 판단 기준으로 삼았습니다.

  • 지금 실제로 사용되지 않는 코드는 남겨두지 않기
  • 외부에서 꼭 필요한 항목만 export하기
  • 공통화는 하되, 내부 콘텐츠/이벤트/상태 로직까지 무리해서 묶지 않기
  • alert도 사용자 경험은 그대로 두고, 내부 호출 구조만 단순하게 만들기

🤔 왜 이 feature를 리팩토링해야겠다고 생각했을까?

앱잼 이후 experience-detail을 다시 살펴보면서 가장 먼저 들었던 생각은, **“기능은 돌아가게 동작하고 있지만, 여러 상태 구조가 꼬여있고 중복된 구조 및 불필요한 구조가 많아서 하나의 feature 내부가 매우 비대해서 좋은 구조를 가진 코드는 아니다”**였습니다. 그래서 feature의 상태 관리 흐름 및 코드 구조 전반적인 부분에 대해 대대적인 리팩토링을 진행해야 겠다고 생각했습니다.

특히 experience-detail feature는 단순 조회 화면이 아니라,

  • 등록(create)
  • 수정(edit)
  • 조회(view)
  • 저장/삭제/대표경험 설정
  • 이탈 방지 처리
  • alert 표시

같은 흐름이 하나의 feature 안에서 함께 맞물려 있어서, 상태 로직 하나만 건드려도 UI, alert, hook, store, page 초기화 로직까지 연쇄적으로 확인해야 하는 구조였습니다. 따라서 이런 상황에서는 곧바로 상태 로직을 손보는 것보다, 먼저 아래와 같은 내용을 정리해두는 편이 더 안전하다고 판단했습니다.

  1. 실제로 안 쓰는 코드를 먼저 제거해서 현재 살아 있는 경로만 남기기
  2. 외부에 무엇을 공개할지 정리해서 feature 경계를 명확히 하기
  3. 중복된 레이아웃 구조를 정리해서 UI 변경 시 확인 포인트 줄이기
  4. alert 호출 구조를 단순화해서 부수 효과 흐름을 더 쉽게 읽히게 만들기

🤔 왜 이 작업을 먼저 진행했을까?

위와 같은 판단 기준을 세운 후, 사전 정리 작업을 진행하려 구체적으로 코드를 뜯어보니 이번 pr에서는 아래와 같은 지점들을 작업해야 겠다고 생각했습니다.

  • 사용되지 않는 hook / function / helper가 일부 남아 있어서, 실제 사용 경로와 과거 구현 흔적이 함께 섞여 있는 상태
  • index.ts를 통해 feature 내부 구현 성격의 코드까지 외부에 함께 노출되고 있어서, “어디까지가 public API인지” 경계가 흐린 상태
  • ExperienceForm / ExperienceViewer가 역할은 다르지만 외부 레이아웃 구조는 거의 비슷해, 이후 UI 수정 시 양쪽을 함께 봐야 하는 중복 포인트가 존재
  • alert도 사용자에게 보이는 흐름 자체는 단순한데, 내부적으로는 store / helper / 도메인 함수가 여러 단계로 연결되어 있어 실제 수정 포인트를 바로 파악하기 어려운 상태

그 후 다음 pr에서는 아래와 같은 상태 로직 변경 작업을 진행해야겠다고 대략적으로 생각했습니다.

  • mode 스토어 → URL 기반 단일화
  • isDraftDirty 버그 수정 (edit 모드 기준 비교 로직 개선)
  • useExperienceHeaderActions 책임별 분리
  • isTransitioning 제거

이 작업들은 단순한 구조 정리가 아니라 상태 흐름 자체를 변경하는 리팩토링이기 때문에, dead code가 남아 있거나, 내부 구현이 외부에 과도하게 노출되어 있거나, 레이아웃/alert/로직이 얽혀 있는 상태에서는 변경 영향 범위를 정확히 파악하기 어렵다고 판단했습니다.

📑 Tasks

✔️ experience-detail 내부 dead code 제거

experience-detail 내부에서 실제로 import / 호출되지 않는 dead code들을 제거했습니다. 특히 hook 버전과 plain function 버전이 함께 존재하던 부분은, 실제 페이지에서 사용되는 경로만 남기고 나머지를 제거했습니다.

제거한 주요 항목 예시는 아래와 같습니다.

  // store/use-experience-hooks.ts
  // 제거 전
  export const useDefaultExperienceId = () => ...
  export const useDefaultButtonLabel = () => ...
  export const useShowEditDeleteButtons = () => ...
  export const useShowSubmitButton = () => ...
  export const useCurrentExperienceId = () => ...

위 selector hook들은 실제 사용처가 없거나, 동일 로직이 useExperienceHeaderActions 내부에서 인라인으로 계산되고 있어 별도 hook로 유지할 필요가 없다고 판단했습니다.


// model/use-init-experience-detail.ts
// 제거 전
export const useInitExperienceDetail = () => {
  return useCallback((mode, experienceId) => {
    ...
  }, []);
};

export const useResetExperienceDetail = () => {
  return useCallback(() => {
    ...
  }, []);
};

export const resetExperienceDetail = () => {
  ...
};

여기서는 실제 페이지에서 hook 버전이 아니라 plain function인 initExperienceDetail만 사용되고 있었고, useResetExperienceDetail / resetExperienceDetail은 어디서도 import되지 않아 제거했습니다.


// model/use-hydrate-experience.ts
// 제거 전
export const useHydrateExperienceFromApi = () => {
  return useCallback((experience) => {
    ...
  }, []);
};

이 부분도 페이지에서는 hook 버전이 아니라 plain function인 hydrateExperienceFromApi만 사용되고 있어,
사용되지 않는 hook만 제거했습니다.


// model/use-actions.ts
// 제거 전
export const useDeleteExperience = () => {
  return {
    targetExperience,
    canDelete,
  };
};

useDeleteExperience는 반환값은 있었지만 실제 사용처가 없었고, 삭제 동작 자체는 useExperienceHeaderActionsonClickDelete가 담당하고 있어 제거했습니다.


// model/use-alert.ts
// 제거 전
export const showExperienceInfo = (...) => ...
export const showExperienceWarning = (...) => ...

위 두 함수는 정의된 파일 외 호출하는 곳이 없어 제거했습니다.


✔️ index.ts 외부 API만 노출하도록 export 정리

기존에는 experience-detail 내부 구현 성격의 hook, util, helper, store까지 index.ts를 통해 함께 export되고 있었는데, 실제 export된 코드가 밖에서 사용되는 곳은 src/pages/experience-detail/experience-detail-page.tsx 1곳뿐이었고, 그 파일에서 사용하는 항목도 제한적이었습니다.

따라서 **“이 feature가 외부에 어떤 API를 제공해야 하는가”**에 초점을 두고 정리했습니다.
정리 전후는 아래와 같습니다.

// before: index.ts에서 내부 구현까지 모두 export
export { useExperienceDetailStore, initialDraft, useExperienceDraft, useExperienceActions, ... } from './store/...';
export { useExperienceSubmit, useExperienceHeaderActions, useExperienceDateField, ... } from './model/...';
export { showSaveError, showDeleteSuccess, showValidationError, ... } from './model/use-alert';
export { formatDateDash, parseYMD } from '@/shared/lib/format-date';
// after: 외부 페이지에서 실제로 사용하는 항목만 유지
export { ExperienceForm } from './ui/experience-form/experience-form';
export { ExperienceViewer } from './ui/experience-viewer/experience-viewer';
export { ExperienceAlertRenderer } from './ui/experience-alert-renderer/experience-alert-renderer';
export { useGetExperienceDetail } from './api/get-experience-detail';
export { useExperienceMode } from './store/use-experience-mode';
export { useLeaveConfirm } from './model/use-leave-confirm';
export { initExperienceDetail } from './model/use-init-experience-detail';
export { hydrateExperienceFromApi } from './model/use-hydrate-experience';
export type { ExperienceMode } from './types/experience-mode';
export { EXPERIENCE_MESSAGES } from './config/messages';

판단 기준은 아래와 같았습니다.

  1. 프로젝트 전체에서 @/features/experience-detail를 import하는 파일 검색
  2. 외부 사용처가 experience-detail-page.tsx 1곳뿐이라는 점 확인
  3. 그 파일에서 실제로 사용하는 항목만 public API로 유지
  4. 그 외 hook / helper / store / util은 export 제거

✔️ ExperienceLayout 공통 컴포넌트 추출

ExperienceFormExperienceViewer를 비교해보니, 내부 콘텐츠는 다르지만 외부 레이아웃 껍데기(page → StickyHeader → outerSection → panel)는 사실상 동일한 역할을 하고 있었습니다.
다만 이때 props 구조나 내부 필드, 이벤트 처리까지 한 번에 공통화하면 오히려 결합도가 높아질 수 있다고 판단해서, 이번에는 외부 shell 레벨까지만 공통화했습니다.

즉, “공통화할 수 있는 모든 것”을 묶기보다는, 중복된 외부 구조만 제거하고 Form / Viewer의 내부 책임은 그대로 유지하는 방향으로 가져갔습니다.

변경 전 구조는 대략 아래와 같았습니다.

// ExperienceForm (변경 전)
<main className={styles.page}>
  <StickyHeader ... />
  <section className={styles.outerSection}>
    <div className={styles.panel}>
      <div className={styles.card}>
        <div className={styles.innerColumn}>
          {/** form content */}
        </div>
      </div>
    </div>
  </section>
</main>
// ExperienceViewer (변경 전)
<main className={styles.page}>
  <StickyHeader ... />
  <section className={styles.outerSection}>
    <div className={styles.panel}>
      {/** viewer content */}
    </div>
  </section>
</main>

변경 후에는 공통 외부 구조를 ExperienceLayout으로 추출했습니다.

// ExperienceLayout
interface ExperienceLayoutProps {
  isDefault: boolean;
  onToggle: () => void;
  rightSlot?: ReactNode;
  children: ReactNode;
}

export const ExperienceLayout = ({
  isDefault,
  onToggle,
  rightSlot,
  children,
}: ExperienceLayoutProps) => {
  return (
    <main className={styles.page}>
      <StickyHeader
        isDefault={isDefault}
        onToggle={onToggle}
        rightSlot={rightSlot}
      />
      <section className={styles.outerSection}>
        <div className={styles.panel}>{children}</div>
      </section>
    </main>
  );
};

그리고 Form / Viewer는 내부 콘텐츠만 넘기도록 정리했습니다.

// ExperienceForm (변경 후)
<ExperienceLayout
  isDefault={isDraftDefault}
  onToggle={onToggleDefault}
  rightSlot={<Button ...>작성완료</Button>}
>
  {/** 기존 form content */}
</ExperienceLayout>
// ExperienceViewer (변경 후)
<ExperienceLayout
  isDefault={isDefault}
  onToggle={onToggleDefault}
  rightSlot={<HeaderActions ... />}
>
  {/** 기존 viewer content */}
</ExperienceLayout>

CSS도 마찬가지로 레이아웃 관련 스타일만 이동시키고, Form / Viewer 고유 스타일은 그대로 남겨두었습니다.

// experience-layout.css.ts
export const page = style({...});
export const outerSection = style({...});
export const panel = style({
  width: '106rem',
  paddingInline: '9rem',
  gap: '8rem',
  ...
});

✔️ alert 구조 단순화 및 내부 구현 캡슐화

experience-detail의 alert 구조를 보면, 외부로 export된 store와 범용 helper를 여러 단계 거쳐 도메인 함수가 호출되는 형태였는데, 실제 사용 범위를 보면 일부는 외부 공개 API라기보다 feature 내부 구현에 가까웠습니다.
그래서 사용자에게 보이는 alert 흐름 자체는 유지하면서, 중간 indirection 레이어를 줄이고 store를 내부 구현으로 더 가깝게 정리하는 방향으로 작업했습니다.

먼저 사용처가 없는 액션을 제거했습니다.

// before
interface ExperienceAlertActions {
  push: (alert: ExperienceAlert) => void;
  remove: (id: string) => void;
  closeAll: () => void;
}
// after
interface ExperienceAlertActions {
  push: (alert: ExperienceAlert) => void;
  remove: (id: string) => void;
}

다음으로 범용 helper 2개를 제거했습니다.

// before
export const showExperienceError = (message: string) => {
  ...
};

export const showExperienceSuccess = (message: string) => {
  ...
};

export const showSaveError = () => showExperienceError(...);
export const showDeleteError = () => showExperienceError(...);
export const showSaveSuccess = (title?: string) =>
  showExperienceSuccess(...);
// after
const showAlert = (params: ExperienceAlertParams) => {
  ...
};

export const showSaveError = () =>
  showAlert({ type: 'error', message: ... });

export const showDeleteError = () =>
  showAlert({ type: 'error', message: ... });

export const showSaveSuccess = (title?: string) =>
  showAlert({
    type: 'success',
    message: `${title ?? '경험'} 저장이 완료되었어요.`,
  });

showExperienceError, showExperienceSuccess 처럼 한 번 더 감싸는 범용 helper는 제거하고, 실제로 필요한 도메인 함수가 내부 showAlert 유틸을 바로 호출하도록 정리했습니다.

추가로 store export 범위도 축소했습니다.

// before
export { useExperienceAlertStore } from './store/use-experience-alert-store';
// after
// useExperienceAlertStore는 내부 구현으로만 사용
// 외부에는 아래만 노출
export { useExperienceAlerts, useExperienceAlertActions } from './model/use-alert';
export { showSaveError, showDeleteError, showSaveSuccess, ... } from './model/use-alert';

이 부분은 “store를 숨기기 위해 숨긴다”기보다는, 이후 alert 내부 구현이 바뀌더라도 바깥에서 store shape에 직접 의존하지 않도록
접근 경계를 조금 더 좁히는 것이 안전하다
고 판단했습니다.

추가로 showSaveSuccess의 기본값 처리도 호출부에서 직접 하도록 바꿨습니다.

// before
showExperienceSuccess(title);
// after
showAlert({
  type: 'success',
  message: `${title ?? '경험'} 저장이 완료되었어요.`,
});

이건 큰 구조 변경은 아니지만, 기본값 처리 위치를 호출 시점으로 더 명확하게 가져오는 편이 읽기 쉽다고 판단했습니다.

👀 To Reviewer

  • 이번 PR은 기능 변경보다는 experience-detail feature 내부 구조를 단계적으로 정리하는 흐름으로 작업했습니다. 그래서 각 변경도 “더 많이 공통화할 수 있는가?”보다는 **“지금 실제로 필요한 범위까지만 정리하는 것이 맞는가?”**를 기준으로 판단했습니다.

  • dead code 제거는 최대한 보수적으로 진행했고, 사용처가 없다는 것이 명확한 항목만 제거했습니다. 혹시 제가 놓친 사용 경로가 있을지 한 번 같이 봐주시면 좋을 것 같습니다 🙇

  • index.ts export 정리는 외부에서 실제 사용하는 항목만 남기고 내부 구현은 feature 안으로 숨기는 방향으로 진행했습니다. public API 경계를 이 정도 수준으로 좁히는 판단이 괜찮은지 의견 주시면 감사하겠습니다!

  • ExperienceLayout은 Form / Viewer의 모든 공통점을 묶기보다는, 우선 외부 레이아웃 shell까지만 공통화했습니다. 내부 콘텐츠나 props 구조까지 같이 추출하면 오히려 책임 경계가 흐려질 수 있다고 봤는데, 공통화 범위를 여기서 끊은 판단이 적절한지도 봐주시면 좋겠습니다.

  • alert 구조는 공용 시스템 전체를 건드리기보다는, experience-detail 내부에서 불필요한 우회 레이어와 과도한 export를 줄이는 방향으로만 정리했습니다. 이 정도 수준의 캡슐화가 괜찮은지, 혹은 이후에는 공용 alert/toast 시스템과 더 맞춰가는 방향이 좋을지도 궁금합니다!

  • PR 범위가 dead code / public API / layout / alert까지 포함되어 있어 다소 넓게 느껴질 수 있을 것 같은데, 모두 “experience-detail 내부 경계와 구조를 정리한다”는 같은 맥락으로 묶어서 가져갔습니다. 혹시 이 묶음이 다소 넓다고 느껴지시면 그 부분도 편하게 말씀 부탁드립니다 ☺️

📸 Screenshot

🔔 ETC

이번 pr은 experience-detail 리팩토링의 중간 단계이며, 후속 pr에서 직접적으로 상태 관리 관련 리팩토링을 이어서 진행할 예정입니다 :)

Summary by CodeRabbit

  • 리팩토링
    • 경험 상세가 새 통합 레이아웃으로 전환되어 상단 헤더 동작과 폼/뷰어 구조가 간소화되고 일부 헤더 액션 위치가 변경되었습니다.
    • 모듈의 공개 인터페이스가 축소되어 외부에서 접근하던 일부 상태·훅·유틸이 더 이상 노출되지 않습니다.
  • 스타일
    • 페이지 배경과 패널 레이아웃을 위한 새로운 스타일 세트가 추가되었습니다.
  • 동작 변경
    • 알림 표시가 표준화된 타이틀("오류"/"완료")과 최근 항목 중심의 중복 검사로 동작 방식이 변경되었습니다.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

experience-detail 모듈의 공개 API가 대폭 축소되었고, 공통 레이아웃 컴포넌트 ExperienceLayout과 해당 스타일이 추가되어 폼/뷰어의 DOM 구조와 스타일이 재배치되었습니다. 초기화·수화·알림 관련 훅들이 파일-레벨 함수 또는 내부 구현으로 대체/비공개화되었습니다. (사실만 기술)

Changes

Cohort / File(s) Summary
공개 API 축소
src/features/experience-detail/index.ts
모듈의 공개 export 대폭 축소 — 대부분의 쿼리/POST/PATCH/DELETE, store 유틸, 액션 훅, 날짜/포맷/유효성/엔티티 변환, 여러 타입 및 DEFAULT_BUTTON_LABELS 등이 제거되고 노출 항목이 useGetExperienceDetail, initExperienceDetail, applyExperienceDetailFromApi, useExperienceMode, EXPERIENCE_MESSAGES, ExperienceMode 등으로 제한됨.
모델: 액션·알림·초기화·수화 변경
src/features/experience-detail/model/use-actions.ts, .../use-alert.ts, .../use-hydrate-experience.ts, .../use-init-experience-detail.ts
useDeleteExperience 및 여러 alert 헬퍼와 closeAll 제거. alert 스토어를 파일-프라이빗으로 변경하고 경고 생성 로직을 showAlert로 통합. 훅 기반 초기화/수화(useInitExperienceDetail, useHydrateExperienceFromApi)는 제거되고 파일-레벨 함수(initExperienceDetail, applyExperienceDetailFromApi)로 대체됨.
스토어 훅/셀렉터 제거
src/features/experience-detail/store/use-experience-hooks.ts, src/features/experience-detail/store/...
파생 셀렉터/훅들(useDefaultExperienceId, useDefaultButtonLabel, useShowEditDeleteButtons, useShowSubmitButton, useCurrentExperienceId) 및 일부 store export(initialDraft 등) 제거/비공개화.
레이아웃 컴포넌트 추가
src/features/experience-detail/ui/experience-layout/experience-layout.tsx, .../experience-layout.css.ts
신규 ExperienceLayout 컴포넌트와 레이아웃 스타일 추가. StickyHeader 통합 및 headerRightContent prop으로 헤더 액션 전달 패턴 도입.
폼/뷰어 리팩토링 (레이아웃 적용)
src/features/experience-detail/ui/experience-form/experience-form.tsx, .../experience-viewer/experience-viewer.tsx
기존 <main>/중첩 섹션·패널 구조와 개별 StickyHeader 사용 제거, ExperienceLayout로 래핑 및 DOM 평탄화. UI 요소(필터/제목/날짜/별점)는 유지하되 위치 변경.
스타일 이동/정리
src/features/experience-detail/ui/experience-form/experience-form.css.ts, .../experience-viewer/experience-viewer.css.ts
공통 레이아웃 스타일(page, outerSection, panel)이 기존 form/viewer 모듈에서 제거되고 experience-layout.css.ts로 이동. 일부 개별 스타일은 유지.
페이지 컴포넌트: 수화 호출·타입 단순화
src/pages/experience-detail/experience-detail-page.tsx
hydrateExperienceFromApi 호출을 applyExperienceDetailFromApi로 교체. 로컬 props 인터페이스 제거 후 인라인 타입 사용.

Sequence Diagram

sequenceDiagram
  participant User as User
  participant Form as ExperienceForm
  participant Layout as ExperienceLayout
  participant Header as StickyHeader
  participant Store as ExperienceStore
  participant API as BackendAPI

  User->>Form: 입력/수정
  Form->>Layout: 제출 트리거 (headerRightContent 버튼)
  Layout->>Header: 버튼 클릭 전달
  Header->>Store: submit 액션 호출
  Store->>API: post/patch 요청
  API-->>Store: 응답
  Store-->>Form: 상태(current, draft) 업데이트
Loading
sequenceDiagram
  participant API as BackendAPI
  participant Hydrator as applyExperienceDetailFromApi
  participant Store as ExperienceStore
  participant Layout as ExperienceLayout
  participant Viewer as ExperienceViewer

  API-->>Hydrator: 상세 응답 데이터
  Hydrator->>Store: toExperienceEntity -> setCurrent -> setDefaultExperienceId -> hydrateDraftFromCurrent
  Store-->>Layout: 상태(isDefault, current, draft)
  Layout->>Viewer: children 렌더링 (Viewer/Form)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

🌟FEAT

Suggested reviewers

  • odukong
  • qowjdals23
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 experience-detail 리팩토링의 주요 변경점(구조 정리, 내부 API 범위 축소)을 정확하고 간결하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 제공된 템플릿 구조를 따르며, 변경 배경, 구체적 작업 내용(dead code 제거, export 정리, ExperienceLayout 추출, alert 구조 단순화), 리뷰 요청사항을 명확히 포함하고 있습니다.
Linked Issues check ✅ Passed PR은 #164의 5가지 to-do 중 4가지(사용하지 않는 코드 제거, index.ts export 정리, ExperienceLayout 공통 컴포넌트 추출, alert 구조 단순화)를 완료했으며, DatePicker UI 변경은 명시적으로 별도 작업으로 언급되어 있습니다.
Out of Scope Changes check ✅ Passed 모든 변경이 experience-detail 내부 구조 정리 범위 내 있으며, 기능 변경이나 무관한 수정은 없습니다. 상태 관리 변경(mode, dirty 체크, 헤더 액션 분리)은 의도적으로 미연기한 후속 작업으로 명시되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added 🔗API api 연동 🛠️REFACTOR 코드 리팩토링 유진🍒 labels Mar 29, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 29, 2026

🚀 빌드 결과

린트 검사 완료
빌드 성공

로그 확인하기
Actions 탭에서 자세히 보기

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx (1)

1-22: 🧹 Nitpick | 🔵 Trivial

타입 import 위치를 맨 아래로 이동하는 것이 좋아요.

코딩 가이드라인에 따르면 타입 import는 import 문의 맨 아래에 위치해야 해요. type ExperienceTypeCode가 일반 import와 함께 있어요.

💡 import 순서 수정 제안
-import {
-  EXPERIENCE_TYPE,
-  type ExperienceTypeCode,
-} from "@/shared/config/experience";
+import { EXPERIENCE_TYPE } from "@/shared/config/experience";
 import { parseYMD } from "@/shared/lib/format-date";
 import { modalStore } from "@/shared/model/store";
 import { ModalBasic, Tooltip } from "@/shared/ui";
 import { Button } from "@/shared/ui/button/button";
 import { Tag } from "@/shared/ui/tag/tag";
 import { Textfield } from "@/shared/ui/textfield/textfield";
 import { HELP_TOOLTIP_CONTENT } from "@/shared/ui/tooltip/tooltip.content";

 import { useExperienceHeaderActions } from "../../model/use-actions";
 import {
   useExperienceCurrent,
   useIsDraftDefault,
 } from "../../store/use-experience-hooks";
 import { DatePicker } from "../date-picker/date-picker";
 import { ExperienceLayout } from "../experience-layout/experience-layout";
 import * as layoutStyles from "../experience-layout/experience-layout.css";

 import * as s from "./experience-viewer.css";
+
+import type { ExperienceTypeCode } from "@/shared/config/experience";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx`
around lines 1 - 22, The import of the type ExperienceTypeCode is currently
mixed with runtime imports; extract or move the type-only import so that all
runtime imports remain first and the type import (e.g., "type
ExperienceTypeCode") appears in a dedicated type-only import block at the bottom
of the import list (after other imports like EXPERIENCE_TYPE, parseYMD,
modalStore, ModalBasic, Tooltip, Button, Tag, Textfield, HELP_TOOLTIP_CONTENT,
useExperienceHeaderActions, useExperienceCurrent, useIsDraftDefault, DatePicker,
ExperienceLayout, layoutStyles, and s) to follow the project's import ordering
guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/experience-detail/ui/experience-layout/experience-layout.tsx`:
- Around line 1-5: The type import for ReactNode is currently at the top; move
"import type { ReactNode } from 'react';" below the other imports (after "import
* as s from './experience-layout.css';") so type-only imports appear after value
imports; update the import order in the file containing StickyHeader and s
(experience-layout.css) accordingly.

---

Outside diff comments:
In `@src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx`:
- Around line 1-22: The import of the type ExperienceTypeCode is currently mixed
with runtime imports; extract or move the type-only import so that all runtime
imports remain first and the type import (e.g., "type ExperienceTypeCode")
appears in a dedicated type-only import block at the bottom of the import list
(after other imports like EXPERIENCE_TYPE, parseYMD, modalStore, ModalBasic,
Tooltip, Button, Tag, Textfield, HELP_TOOLTIP_CONTENT,
useExperienceHeaderActions, useExperienceCurrent, useIsDraftDefault, DatePicker,
ExperienceLayout, layoutStyles, and s) to follow the project's import ordering
guideline.
🪄 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

Run ID: 0acb4631-53f2-444d-ad9b-78b4e3471c29

📥 Commits

Reviewing files that changed from the base of the PR and between 7d08abb and 7adcf48.

📒 Files selected for processing (12)
  • src/features/experience-detail/index.ts
  • src/features/experience-detail/model/use-actions.ts
  • src/features/experience-detail/model/use-alert.ts
  • src/features/experience-detail/model/use-hydrate-experience.ts
  • src/features/experience-detail/model/use-init-experience-detail.ts
  • src/features/experience-detail/store/use-experience-hooks.ts
  • src/features/experience-detail/ui/experience-form/experience-form.css.ts
  • src/features/experience-detail/ui/experience-form/experience-form.tsx
  • src/features/experience-detail/ui/experience-layout/experience-layout.css.ts
  • src/features/experience-detail/ui/experience-layout/experience-layout.tsx
  • src/features/experience-detail/ui/experience-viewer/experience-viewer.css.ts
  • src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx
💤 Files with no reviewable changes (6)
  • src/features/experience-detail/ui/experience-form/experience-form.css.ts
  • src/features/experience-detail/model/use-actions.ts
  • src/features/experience-detail/ui/experience-viewer/experience-viewer.css.ts
  • src/features/experience-detail/model/use-hydrate-experience.ts
  • src/features/experience-detail/model/use-init-experience-detail.ts
  • src/features/experience-detail/store/use-experience-hooks.ts

Comment thread src/features/experience-detail/ui/experience-layout/experience-layout.tsx Outdated
@u-zzn u-zzn changed the title Refactor/#164/experience detail cleanup [Refactor] experience-detail 구조 정리 및 내부 API 범위 축소 Mar 29, 2026
Copy link
Copy Markdown
Collaborator

@odukong odukong left a comment

Choose a reason for hiding this comment

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

불필요하게 정의된 코드를 덜어내고 더 나은 코드 개선을 위한 베이스 작업으로서 전반적인 수정이 이루어져야하는 필수적인 상황이었기 때문에 괜찮다고 생각합니다! (사이즈도 적절하고요!)
사용하지 않는 코드들은 선제적으로 제거해주어서 이 이후 리팩토링에 있어 가독성이나 번들 크기에서도 개선이 될 것 같네요!

PR 관련하여 몇 가지 의견 남겨두었으니 확인 부탁드리겠습니다!! φ(゜▽゜*)♪

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

index.ts는 지금처럼 features 외부에서 참조하는 파일에 대해서만 export하고 그 외는 내부에서 상대경로로 참조하는 것으로 충분하다고 생각합니다. 해당 방향이 저희가 정리한 컨벤션이기도 하고요!

한 가지 현재 단계에서 수정하면 좋을 점은 experience-detail-page.tsx에서 이미 index.ts에서 mode에 대한 type을 호출하여 사용하고 있는데 한번 더 props 타입으로 감쌀 필요는 없다고 생각해요! ExperienceMode로 대체해서 사용해도 충분하다고 생각합니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

그러네요..! 불필요한 wrapping이 한 단계 더 생긴 셈이라 말씀해주신 방향대로 ExperienceMode를 직접 사용하는 형태로 수정해보겠습니다 :)

Comment on lines +14 to +26
const ExperienceLayout = ({
isDefault,
onToggle,
rightSlot,
children,
}: ExperienceLayoutProps) => {
return (
<main className={s.page}>
<StickyHeader
isDefault={isDefault}
onToggle={onToggle}
rightSlot={rightSlot}
/>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

기존에는 view/edit 페이지의 stickyheader에서 사용되는 액션이 다르다보니 각각 중복적으로 선언하게 되었던 것인데, ExperienceLayout를 통해 하나의 레이아웃으로 재사용할 수 있게 되어 좋다고 생각이 듭니다!

다만 기존의 rightSlot이라는 prop명은 stickyHeader 자체의 prop으로는 자연스럽지만, layout에서 넘기는 prop명으로는 다소 모호하게 느껴질 수 있다고 생각이 들어, 좀 더 그 의미가 명확해질 수 있도록 수정하면 좋을 것 같습니당!!

외부 레이아웃까지만 공통화해주셨다고 pr에 작성해주셨는데, 각 컨텐츠까지 공통화를 하게되면 그 props도 당연히 많아질 수 밖에 없고, 그러면 또 그 만큼 내부 분기문도 많아져 오히려 가독성이 떨어질 수 있기 때문에, 지금의 방식을 유지하는 것이 좋다고 느껴집니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

좋은 지적 감사합니다! 🙂
말씀 주신 것처럼 rightSlot은 StickyHeader 내부 prop으로는 자연스럽지만, Layout에서 받는 이름으로는 다소 모호할 수 있을 것 같아서 의미가 더 잘 드러나는 headerRightContent로 이름 수정해두겠습니다 :)

Comment on lines +24 to +28
const useExperienceAlertStore = create<ExperienceAlertState>((set, get) => ({
alerts: [],
actions: {
show: (variant, title, description) => {
const { alerts } = get();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

현재 useExperienceAlertStore는 파일명 자체는 use-alert.tsx로 정의되어있지만, 경험 작성 페이지에 종속된 alert 스토어에 가깝다고 느껴져요.
개인적인 의견으로는 alert는 사실상 특정 페이지에만 사용되는 성격이 아니기 때문에 shared/store로 분리하는 것이 적절하다고 생각이 듭니다!

modal과 마찬가지로 AlertRenderer를 modal-provider처럼 전역에서 렌더링해주는 방향으로 수정해주어도 좋을 것 같다는 생각이 듭니다! alert-store를 스토어로 선언하여 전역으로 관리하는 것 자체가 옵저버 패턴 적용을 위한 기반이 되기도 하고, 만약 같은 방향으로 구성하지 않더라도 shared/store 전역으로 분리하는 것으로도 충분하다고 생각해요! (물론 다음 스프린트에서 가도 허허..)

Copy link
Copy Markdown
Collaborator Author

@u-zzn u-zzn Apr 4, 2026

Choose a reason for hiding this comment

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

좋은 포인트 짚어주셔서 감사합니다 🙇

말씀해주신 것처럼 현재 useExperienceAlertStore는 experience-detail 내부에 위치해 있긴 하지만, 역할 자체는 특정 페이지에 종속된 로직이라기보다는 alert 상태를 관리하는 성격에 더 가깝다고 느껴져서 저 또한 shared/store로 분리하는 방향이 더 적절하다구 생각합니다! 또한 AlertRenderer를 modal-provider처럼 전역에서 렌더링하는 구조로 가져가는 부분도, alert를 하나의 전역 관심사로 다루는 점에서 의미 있다구 생각합니다 :)

다만 이번 PR에서는 “기존 동작을 유지한 상태에서 내부 구조를 정리하는 것”을 목표로 잡고 작업을 진행하다 보니, alert를 shared 레벨로 올리는 작업까지 포함하기에는 범위가 다소 커진다고 판단했습니다.

그래서 이번 단계에서는 experience-detail 내부에서 불필요한 abstraction을 줄이고 export 범위를 정리하는 수준까지만 반영하였고, 말씀해주신 shared/store 분리 및 전역 renderer 구조로의 개선은 다음 스프린트에서 조금 더 구조적으로 고민해보겠습니다 ㅠ!! 좋은 방향 제안해주셔서 감사합니다! 👍🖤

Comment on lines 82 to 86
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

또한 useExperienceAlertsuseExperienceAlertActions는 별도로 분리하는 것은 분명히 여러 파일에서 사용이 간단하게 하기 위함일텐데 지금의 규모라면, 분리하지 않고 사용하는 쪽에서 직접 selector로 접근하는 것으로 충분하다고 생각이 듭니다! (특히 alerts는 AlertRenderer에서만 접근하고 있기도 하고요!!)
⬅️이 부분은 만약 완전히 alert-store를 전역으로 관리하게 되면 자연스럽게 제거될 부분일 것 같긴 하네요!

Comment on lines 6 to 8
export const hydrateExperienceFromApi = (data: GetExperienceDetailResponse) => {
const { actions } = useExperienceDetailStore.getState();
const entity = toExperienceEntity(data);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

hydrateExperienceFromApi는 API 응답 데이터를 내부에서 사용하는 타입으로 포맷팅하는 역할로써 동작을 하기 떄문에 API 스펙이 변경되더라도 해당 함수만 수정하면 된다는 점에서 좋은 구조라고 생각합니다!

다만 함수명에 있어 그 역할이 직관적으로 와닿지 않아서(제가 영어를 사랑하지 않는 사람이라 그런 ... 거 같기도 하네요..), 포맷팅의 의도가 명확하게 드러나는 이름으로 변경하면 가독성에 도움이 될 것 같습니다!

Copy link
Copy Markdown
Collaborator Author

@u-zzn u-zzn Apr 4, 2026

Choose a reason for hiding this comment

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

넵!! 🙂
“API 데이터를 받아 현재 상태에 반영한다”는 의미가 드러나는applyExperienceDetailFromApi 이름으로 수정해보겠습니다아 :) 혹시 다들 더 좋은 의견 있으시다면 편하게 알려주세요!

Copy link
Copy Markdown
Collaborator

@qowjdals23 qowjdals23 left a comment

Choose a reason for hiding this comment

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

단순히 안 쓰는 코드만 정리한 느낌보다는 다음 상태 관리 리팩토링 전에 experience-detail의 경계와 구조를 먼저 한 번 정돈해두는 흐름이 잘 보여서 좋았습니다 😊

한 번에 많은 걸 공통화하기보다는 이번 단계에서 필요한 범위까지만 정리해두신 점도 좋았고,
dead code 정리, index.ts 공개 범위 축소, ExperienceLayout으로 shell 레벨 공통화, alert 호출 구조 단순화까지 PR 설명도꼼꼼해서 이해하기 편했어요 !!!! 👍

작업량 많으셨을 것 같은데 수고 많으셨습니다 !!!~!! 🏊🏊

Comment on lines 34 to 41
if (!current) {
return (
<main className={s.page}>
<div className={s.outerSection}>
<main className={layoutStyles.page}>
<div className={layoutStyles.outerSection}>
<p>경험 정보를 불러오는 중...</p>
</div>
</main>
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ExperienceLayout으로 외부 구조 정리해주신 방향 너무 좋은 것 같습니다 :)
다만 여기서는 !current일 때만 layout을 직접 가져다 쓰고 있어서, 이 부분은 혹시 의도적으로 분리해두신 건지 궁금했습니다 !!
가능하다면 이 부분도 ExperienceLayout을 같이 쓰도록 맞춰두면 전체 흐름이 더 자연스럽게 보일 수 있을 것 같은데 어떻게 생각하시나요? 🙌

Copy link
Copy Markdown
Collaborator Author

@u-zzn u-zzn Apr 4, 2026

Choose a reason for hiding this comment

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

해당 부분은 로딩 상태에서는 header나 상단 액션이 필요하지 않다고 생각해서 최소한의 구조만 유지하는 쪽으로 분리해두었는데, 말씀 주신 것처럼 동일한 레이아웃을 유지하는 편이 전체적인 구조 흐름상 더 자연스러울 것 같습니다 👍

!current인 경우에도 ExperienceLayout을 사용하도록 맞추는 방향으로 수정해보겠습니다! 감사합니다 ☺️

Comment on lines +29 to +35
const isDuplicate = alerts.some(
(a) =>
a.variant === variant &&
a.title === title &&
a.description === description
);
if (isDuplicate) return;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

지금 variant / title / description이 같으면 동일한 alert을 모두 막는 형태라서 저장이나 삭제를 다시 시도하는 경우처럼 문구를 다시 보여주고 싶은 상황도 같이 막힐 수 있을 것 같아 보여요 ! 이 부분도 혹시 의도하신 동작인지 궁금합니다 !
만약 아니라면 전체 alert 기준보다는 직전에 뜬 alert만 비교하는 방식으로 좁혀보는 건 어떻게 생각하시나요 ?!

Copy link
Copy Markdown
Collaborator Author

@u-zzn u-zzn Apr 4, 2026

Choose a reason for hiding this comment

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

말씀해주신 것처럼 현재는 variant / title / description이 같은 alert를 store 전체 기준으로 막고 있어서, 저장이나 삭제를 다시 시도하는 경우처럼 같은 문구를 의도적으로 한 번 더 보여주고 싶은 상황까지 함께 막을 수 있을 것 같네요 😥

처음에는 동일한 alert가 짧은 시간 안에 중복으로 쌓이는 것만 방지하려는 의도로 넣었던 로직이었는데, 지금은 중복 방지 범위가 넓어서 실제 사용자가 원하는 흐름까지 제약할 수 있을 것 같습니다.

그래서 말씀 주신 것처럼 전체 alerts를 기준으로 비교하기보다는, 직전에 표시된 alert만 비교하는 방향으로 범위를 좁히는 쪽으로 수정해보겠습니다! 감사합니다 ☺️

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx (1)

34-41: 🧹 Nitpick | 🔵 Trivial

로딩 상태에서도 ExperienceLayout을 사용하는 것을 고려해 주세요.

현재 !current 조건에서는 layoutStyles를 직접 사용하고 있는데, 이전 리뷰에서도 언급된 것처럼 ExperienceLayout을 일관되게 사용하면 전체 흐름이 더 자연스러워질 수 있어요. 다만, 로딩 상태에서는 isDefaultonToggle 값이 없으므로 의도적으로 분리하신 것으로 보여요.

만약 일관성을 위해 변경하신다면, 로딩 상태용 기본값을 전달하거나 ExperienceLayout에 로딩 상태를 위한 props를 추가하는 방법을 고려해 볼 수 있어요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx`
around lines 34 - 41, The loading branch currently returns a standalone layout
using layoutStyles instead of the shared ExperienceLayout; change the return to
render ExperienceLayout (the same component used for the main view) and move the
loading message into its children, supplying safe defaults for missing props
(e.g., isDefault={false} and a no-op onToggle) or extend ExperienceLayout to
accept an isLoading prop so it can render a centered "경험 정보를 불러오는 중..." state;
update the conditional that checks current to render <ExperienceLayout
...>...</ExperienceLayout> and ensure you reference the existing
ExperienceLayout component, current variable, and props isDefault/onToggle when
making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/experience-detail/ui/experience-form/experience-form.tsx`:
- Around line 58-65: The title input (value bound to draft.title and updated via
setDraftField) lacks an accessible label; add one by either rendering a <label>
for that input (visually hidden via a utility CSS class) or adding an
aria-label/aria-labelledby attribute to the input (the element using class
s.titleInput) so screen readers can announce the field; ensure the label text
matches the placeholder meaning (e.g., "제목") and that the input keeps maxLength
and onChange behavior unchanged.

In `@src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx`:
- Around line 102-118: The DatePicker instances pass an empty inline handler
onChangeSelectedDate={() => {}} while disabled; replace these with a shared noop
function or make the DatePicker prop optional when disabled. Add a module-level
noop (e.g., const noop = () => {}) and use onChangeSelectedDate={noop} for the
DatePicker usages in experience-viewer, or update the DatePicker prop type to
make onChangeSelectedDate optional and omit it when disabled; reference the
DatePicker component and its onChangeSelectedDate prop when applying the change.

---

Outside diff comments:
In `@src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx`:
- Around line 34-41: The loading branch currently returns a standalone layout
using layoutStyles instead of the shared ExperienceLayout; change the return to
render ExperienceLayout (the same component used for the main view) and move the
loading message into its children, supplying safe defaults for missing props
(e.g., isDefault={false} and a no-op onToggle) or extend ExperienceLayout to
accept an isLoading prop so it can render a centered "경험 정보를 불러오는 중..." state;
update the conditional that checks current to render <ExperienceLayout
...>...</ExperienceLayout> and ensure you reference the existing
ExperienceLayout component, current variable, and props isDefault/onToggle when
making the change.
🪄 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

Run ID: 9fd1d9f2-b25e-436f-83cc-0823f408dc81

📥 Commits

Reviewing files that changed from the base of the PR and between 7adcf48 and 6808eb4.

📒 Files selected for processing (3)
  • src/features/experience-detail/ui/experience-form/experience-form.tsx
  • src/features/experience-detail/ui/experience-layout/experience-layout.tsx
  • src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx

Comment on lines +58 to +65
<input
className={s.titleInput}
type="text"
value={draft.title}
onChange={(e) => setDraftField("title", e.target.value)}
placeholder="제목을 입력해주세요."
maxLength={30}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

입력 필드의 접근성을 확인해 주세요.

title input에 placeholder는 있지만 연결된 <label> 요소가 없어요. 스크린 리더 사용자를 위해 시각적으로 숨겨진 label을 추가하거나 aria-label 속성을 사용하는 것을 권장해요.

♿ 접근성 개선 예시
 <div className={s.titleRow}>
+  <label htmlFor="experience-title" className="sr-only">경험 제목</label>
   <input
+    id="experience-title"
     className={s.titleInput}
     type="text"
     value={draft.title}
     onChange={(e) => setDraftField("title", e.target.value)}
     placeholder="제목을 입력해주세요."
     maxLength={30}
   />

또는 간단하게:

   <input
     className={s.titleInput}
     type="text"
+    aria-label="경험 제목"
     value={draft.title}
📝 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.

Suggested change
<input
className={s.titleInput}
type="text"
value={draft.title}
onChange={(e) => setDraftField("title", e.target.value)}
placeholder="제목을 입력해주세요."
maxLength={30}
/>
<input
id="experience-title"
aria-label="경험 제목"
className={s.titleInput}
type="text"
value={draft.title}
onChange={(e) => setDraftField("title", e.target.value)}
placeholder="제목을 입력해주세요."
maxLength={30}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/experience-detail/ui/experience-form/experience-form.tsx` around
lines 58 - 65, The title input (value bound to draft.title and updated via
setDraftField) lacks an accessible label; add one by either rendering a <label>
for that input (visually hidden via a utility CSS class) or adding an
aria-label/aria-labelledby attribute to the input (the element using class
s.titleInput) so screen readers can announce the field; ensure the label text
matches the placeholder meaning (e.g., "제목") and that the input keeps maxLength
and onChange behavior unchanged.

Comment on lines +102 to +118
<DatePicker
selectedDate={startDate}
onChangeSelectedDate={() => {}}
placeholder="시작일"
disabled
/>

<span className={s.dateDash} aria-hidden="true">
</span>

<DatePicker
selectedDate={endDate}
onChangeSelectedDate={() => {}}
placeholder="종료일"
disabled
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

비활성화된 DatePicker의 빈 핸들러 함수를 개선할 수 있어요.

disabled 상태인 DatePicker에 onChangeSelectedDate={() => {}} 형태의 빈 함수를 전달하고 있어요. 기능적으로는 문제없지만, 공통 noop 함수를 사용하거나 DatePicker 컴포넌트에서 disabled일 때 핸들러를 optional로 처리하면 더 깔끔해질 수 있어요.

💡 개선 예시
// Option 1: 공통 noop 함수 사용
const noop = () => {};

<DatePicker
  selectedDate={startDate}
  onChangeSelectedDate={noop}
  placeholder="시작일"
  disabled
/>

또는 DatePicker 컴포넌트에서 disabled일 때 onChangeSelectedDate를 optional로 만드는 방법도 있어요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx`
around lines 102 - 118, The DatePicker instances pass an empty inline handler
onChangeSelectedDate={() => {}} while disabled; replace these with a shared noop
function or make the DatePicker prop optional when disabled. Add a module-level
noop (e.g., const noop = () => {}) and use onChangeSelectedDate={noop} for the
DatePicker usages in experience-viewer, or update the DatePicker prop type to
make onChangeSelectedDate optional and omit it when disabled; reference the
DatePicker component and its onChangeSelectedDate prop when applying the change.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/experience-detail/model/use-hydrate-experience.ts`:
- Line 6: The exported function applyExperienceDetailFromApi is no longer a hook
but the filename use-hydrate-experience.ts still suggests a hook; rename the
module to a descriptive name such as apply-experience-detail-from-api.ts and
update all imports referencing use-hydrate-experience to the new filename so
consumers import { applyExperienceDetailFromApi } from
'.../apply-experience-detail-from-api'; ensure any barrel/index exports are
updated accordingly.
🪄 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

Run ID: a60214d6-023c-4f2a-b9d2-2ec1e4de0e95

📥 Commits

Reviewing files that changed from the base of the PR and between 6808eb4 and cb8e344.

📒 Files selected for processing (3)
  • src/features/experience-detail/index.ts
  • src/features/experience-detail/model/use-hydrate-experience.ts
  • src/pages/experience-detail/experience-detail-page.tsx

};

export const hydrateExperienceFromApi = (data: GetExperienceDetailResponse) => {
export const applyExperienceDetailFromApi = (data: GetExperienceDetailResponse) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

이제 훅이 아닌 만큼 모듈명도 함께 정리하는 편이 좋겠습니다.

export는 일반 함수로 바뀌었는데 파일명이 여전히 use-hydrate-experience.ts라 훅처럼 읽힙니다. apply-experience-detail-from-api.ts처럼 역할이 드러나는 이름으로 맞추면 탐색성과 오해 방지에 더 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/experience-detail/model/use-hydrate-experience.ts` at line 6,
The exported function applyExperienceDetailFromApi is no longer a hook but the
filename use-hydrate-experience.ts still suggests a hook; rename the module to a
descriptive name such as apply-experience-detail-from-api.ts and update all
imports referencing use-hydrate-experience to the new filename so consumers
import { applyExperienceDetailFromApi } from
'.../apply-experience-detail-from-api'; ensure any barrel/index exports are
updated accordingly.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx`:
- Around line 83-143: Replace non-semantic wrappers with appropriate HTML5
semantic elements: change the outer <div className={s.topGroup}> to a <header>
(preserve className s.topGroup), use a <div> or <nav> only if necessary but
prefer <div className={s.topRow}> -> keep as a <div> or convert to a <div
role="presentation">, change <div className={s.titleRow}> to a <div> or keep but
ensure the main title <h1 className={s.viewerTitle}> remains the primary
heading, and convert <div className={s.starGroup}> to a <section
aria-labelledby="star-group-title"> (preserve className s.starGroup) with each
<div className={s.starField}> changed to <article> or <section> (preserve
className s.starField) so each STAR block (Situation/Task/Action/Result) is a
semantic content region; keep Tooltip, Tag, DatePicker, Textfield usage and
props (HELP_TOOLTIP_CONTENT, startDate, endDate,
current.situation/task/action/result) unchanged and retain attributes like
aria-hidden and disabled while keeping className values for styling.
🪄 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

Run ID: c7d70238-b1af-4fdb-961e-69be7dc13cf7

📥 Commits

Reviewing files that changed from the base of the PR and between cb8e344 and 248aee2.

📒 Files selected for processing (1)
  • src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx

Comment on lines +83 to +143
<div className={s.topGroup}>
<div className={s.topRow}>
<Tag type="register">{typeLabel}</Tag>
</div>

<section className={s.outerSection}>
<div className={s.panel}>
<div className={s.topGroup}>
<div className={s.topRow}>
<Tag type="register">{typeLabel}</Tag>
</div>

<div className={s.titleRow}>
<h1 className={s.viewerTitle}>{current.title}</h1>

<div className={s.tooltipWrap}>
<Tooltip type="help" label="도움말">
{HELP_TOOLTIP_CONTENT}
</Tooltip>
</div>
</div>

<div className={s.dateRow}>
<DatePicker
selectedDate={startDate}
onChangeSelectedDate={() => {}}
placeholder="시작일"
disabled
/>

<span className={s.dateDash} aria-hidden="true">
</span>

<DatePicker
selectedDate={endDate}
onChangeSelectedDate={() => {}}
placeholder="종료일"
disabled
/>
</div>
</div>
<div className={s.titleRow}>
<h1 className={s.viewerTitle}>{current.title}</h1>

<div className={s.starGroup}>
<div className={s.starField}>
<p className={s.starLabel}>Situation (상황)</p>
<Textfield
type="situation"
mode="view"
value={current.situation}
/>
</div>

<div className={s.starField}>
<p className={s.starLabel}>Task (과제)</p>
<Textfield type="task" mode="view" value={current.task} />
</div>

<div className={s.starField}>
<p className={s.starLabel}>Action (행동)</p>
<Textfield type="action" mode="view" value={current.action} />
</div>

<div className={s.starField}>
<p className={s.starLabel}>Result (결과)</p>
<Textfield type="result" mode="view" value={current.result} />
</div>
<div className={s.tooltipWrap}>
<Tooltip type="help" label="도움말">
{HELP_TOOLTIP_CONTENT}
</Tooltip>
</div>
</div>
</section>
</main>

<div className={s.dateRow}>
<DatePicker
selectedDate={startDate}
onChangeSelectedDate={() => {}}
placeholder="시작일"
disabled
/>

<span className={s.dateDash} aria-hidden="true">
</span>

<DatePicker
selectedDate={endDate}
onChangeSelectedDate={() => {}}
placeholder="종료일"
disabled
/>
</div>
</div>

<div className={s.starGroup}>
<div className={s.starField}>
<p className={s.starLabel}>Situation (상황)</p>
<Textfield
type="situation"
mode="view"
value={current.situation}
/>
</div>

<div className={s.starField}>
<p className={s.starLabel}>Task (과제)</p>
<Textfield type="task" mode="view" value={current.task} />
</div>

<div className={s.starField}>
<p className={s.starLabel}>Action (행동)</p>
<Textfield type="action" mode="view" value={current.action} />
</div>

<div className={s.starField}>
<p className={s.starLabel}>Result (결과)</p>
<Textfield type="result" mode="view" value={current.result} />
</div>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

div 중심 마크업을 시맨틱 태그로 바꿔주세요.

변경된 구간에서 레이아웃/콘텐츠 블록이 대부분 div로만 구성되어 있어 구조 의미가 약합니다. section, header, article 등으로 바꾸면 접근성과 유지보수성이 개선됩니다.

예시 diff
-      <div className={s.topGroup}>
-        <div className={s.topRow}>
+      <section className={s.topGroup} aria-label="경험 기본 정보">
+        <header className={s.topRow}>
           <Tag type="register">{typeLabel}</Tag>
-        </div>
+        </header>

-        <div className={s.titleRow}>
+        <div className={s.titleRow}>
           <h1 className={s.viewerTitle}>{current.title}</h1>
           ...
         </div>

-        <div className={s.dateRow}>
+        <section className={s.dateRow} aria-label="경험 기간">
           ...
-        </div>
-      </div>
+        </section>
+      </section>

-      <div className={s.starGroup}>
-        <div className={s.starField}>
+      <section className={s.starGroup} aria-label="STAR 내용">
+        <article className={s.starField}>
           ...
-        </div>
+        </article>
         ...
-      </div>
+      </section>

As per coding guidelines "Use semantic HTML tags instead of generic div elements".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx`
around lines 83 - 143, Replace non-semantic wrappers with appropriate HTML5
semantic elements: change the outer <div className={s.topGroup}> to a <header>
(preserve className s.topGroup), use a <div> or <nav> only if necessary but
prefer <div className={s.topRow}> -> keep as a <div> or convert to a <div
role="presentation">, change <div className={s.titleRow}> to a <div> or keep but
ensure the main title <h1 className={s.viewerTitle}> remains the primary
heading, and convert <div className={s.starGroup}> to a <section
aria-labelledby="star-group-title"> (preserve className s.starGroup) with each
<div className={s.starField}> changed to <article> or <section> (preserve
className s.starField) so each STAR block (Situation/Task/Action/Result) is a
semantic content region; keep Tooltip, Tag, DatePicker, Textfield usage and
props (HELP_TOOLTIP_CONTENT, startDate, endDate,
current.situation/task/action/result) unchanged and retain attributes like
aria-hidden and disabled while keeping className values for styling.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/experience-detail/model/use-alert.ts`:
- Around line 66-83: Extract the repeated title literals ("오류", "완료") into
UPPER_SNAKE_CASE constants (e.g., ALERT_TITLE_ERROR, ALERT_TITLE_SUCCESS) and
use those constants in the functions
showSaveError/showDeleteError/showDefaultSettingError and
showSaveSuccess/showDeleteSuccess (and any other showAlert callers), replacing
the literal strings; ensure showSaveSuccess still supports the optional title
override (title ?? ALERT_TITLE_SUCCESS).
🪄 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

Run ID: 813bdb1b-5f53-4775-a4a9-06ee142a6993

📥 Commits

Reviewing files that changed from the base of the PR and between 248aee2 and 68f98fb.

📒 Files selected for processing (1)
  • src/features/experience-detail/model/use-alert.ts

Comment on lines +66 to 83
showAlert("error", "오류", EXPERIENCE_MESSAGES.API.SAVE_FAILED);
};

export const showDeleteError = () => {
showExperienceError(EXPERIENCE_MESSAGES.API.DELETE_FAILED);
showAlert("error", "오류", EXPERIENCE_MESSAGES.API.DELETE_FAILED);
};

export const showDefaultSettingError = () => {
showExperienceError(EXPERIENCE_MESSAGES.API.DEFAULT_SETTING_FAILED);
showAlert("error", "오류", EXPERIENCE_MESSAGES.API.DEFAULT_SETTING_FAILED);
};

export const showSaveSuccess = (title?: string) => {
showExperienceSuccess(EXPERIENCE_MESSAGES.SUCCESS.SAVED, title);
showAlert("success", title ?? "완료", EXPERIENCE_MESSAGES.SUCCESS.SAVED);
};

export const showDeleteSuccess = () => {
showExperienceSuccess(EXPERIENCE_MESSAGES.SUCCESS.DELETED);
showAlert("success", "완료", EXPERIENCE_MESSAGES.SUCCESS.DELETED);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

오류/완료 타이틀 문자열은 상수로 추출해 중복을 줄여주세요.

동일 리터럴이 여러 곳에 반복되어 문구 변경 시 누락 위험이 있습니다.

♻️ 제안 diff
 let alertIdCounter = 0;
+const ERROR_TITLE = "오류";
+const SUCCESS_TITLE = "완료";
@@
 export const showSaveError = () => {
-  showAlert("error", "오류", EXPERIENCE_MESSAGES.API.SAVE_FAILED);
+  showAlert("error", ERROR_TITLE, EXPERIENCE_MESSAGES.API.SAVE_FAILED);
 };
@@
 export const showDeleteError = () => {
-  showAlert("error", "오류", EXPERIENCE_MESSAGES.API.DELETE_FAILED);
+  showAlert("error", ERROR_TITLE, EXPERIENCE_MESSAGES.API.DELETE_FAILED);
 };
@@
 export const showDefaultSettingError = () => {
-  showAlert("error", "오류", EXPERIENCE_MESSAGES.API.DEFAULT_SETTING_FAILED);
+  showAlert("error", ERROR_TITLE, EXPERIENCE_MESSAGES.API.DEFAULT_SETTING_FAILED);
 };
@@
 export const showSaveSuccess = (title?: string) => {
-  showAlert("success", title ?? "완료", EXPERIENCE_MESSAGES.SUCCESS.SAVED);
+  showAlert("success", title ?? SUCCESS_TITLE, EXPERIENCE_MESSAGES.SUCCESS.SAVED);
 };
@@
 export const showDeleteSuccess = () => {
-  showAlert("success", "완료", EXPERIENCE_MESSAGES.SUCCESS.DELETED);
+  showAlert("success", SUCCESS_TITLE, EXPERIENCE_MESSAGES.SUCCESS.DELETED);
 };

As per coding guidelines, "Use UPPER_SNAKE_CASE for constants (e.g., VITE_API_KEY, ROTATE_DELAY)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/experience-detail/model/use-alert.ts` around lines 66 - 83,
Extract the repeated title literals ("오류", "완료") into UPPER_SNAKE_CASE constants
(e.g., ALERT_TITLE_ERROR, ALERT_TITLE_SUCCESS) and use those constants in the
functions showSaveError/showDeleteError/showDefaultSettingError and
showSaveSuccess/showDeleteSuccess (and any other showAlert callers), replacing
the literal strings; ensure showSaveSuccess still supports the optional title
override (title ?? ALERT_TITLE_SUCCESS).

@u-zzn u-zzn requested review from odukong and qowjdals23 April 4, 2026 18:03
Copy link
Copy Markdown
Collaborator

@odukong odukong left a comment

Choose a reason for hiding this comment

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

수정된 부분 확인했습니다!!! 충돌만 해결하고 머지부탁드려요!!

Copy link
Copy Markdown
Collaborator

@qowjdals23 qowjdals23 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다 ~~~ 어푸할게용 🏊‍♂️🏊‍♀️

@u-zzn
Copy link
Copy Markdown
Collaborator Author

u-zzn commented Apr 8, 2026

확인 감사합니다 :)
토끼친구가 남겨준 리뷰까지 모두 반영하면 pr 규모가 너무 커질 것 같아서, 해당 부분들은 별도의 pr로 나눠서 순차적으로 리팩토링 진행해보도록 하겠습니다 ☺️💪

@u-zzn u-zzn merged commit 3b3ebf8 into dev Apr 8, 2026
3 checks passed
@u-zzn u-zzn deleted the refactor/#164/experience-detail-cleanup branch April 8, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

유진🍒 🔗API api 연동 🛠️REFACTOR 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] 경험 등록/조회/수정 페이지 구조 정리 및 UI 단순화

3 participants