[feat] 프론트엔드 주요 기능 구현 및 UI 개선 - #398
Conversation
[refactor] 사용하지 않는 NotFoundLayout 제거 및 설정 파일 정리
[feat] 스토리북 세팅 및 chromatic 배포
[Feat] Button 컴포넌트 스토리북 스토리 추가
feat: 페이지네이션 스토리북 추가 (#37)
[Refactor] 전략 상세페이지 리팩토링
[refactor] Input 컴포넌트 리팩토링 및 TextField 통합
[fix] S3 이미지 로딩 실패 시 대체 UI 및 로컬 아이콘 적용
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 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 |
리뷰어 가이드이 PR은 입력 및 이미지 프리미티브를 통합하고, 복원력 있는 로컬 전략 아이콘을 추가하며, 전략 상세 권한 부여 및 분석 흐름을 개선하고, 에셋 및 레이아웃 동작을 최적화하고, Storybook/Chromatic 시각적 회귀 테스트 도구를 도입하여 프론트엔드 기능과 UI를 전반적으로 개선합니다. 복원력 있는 이미지 로딩을 위한 시퀀스 다이어그램sequenceDiagram
participant Component
participant StrategyIcon
participant SafeImage
participant Browser
participant Placeholder
Component->>StrategyIcon: getIconSource(src, label)
StrategyIcon->>SafeImage: render resolved src
SafeImage->>Browser: load image
alt image loads
Browser-->>SafeImage: onLoad
SafeImage-->>Component: display image
else image fails
Browser-->>SafeImage: onError
SafeImage->>SafeImage: setFailed(true)
SafeImage->>Browser: load fallbackSrc
alt fallback loads
Browser-->>SafeImage: onLoad
SafeImage-->>Component: display fallback image
else fallback fails
Browser-->>SafeImage: onError
SafeImage->>SafeImage: setFallbackFailed(true)
SafeImage-->>Placeholder: render role img placeholder
end
end
파일 수준 변경 사항
팁 및 명령어Sourcery 사용하기
사용 환경 맞춤 설정대시보드에 액세스하여 다음을 수행할 수 있습니다.
도움말Original review guide in EnglishReviewer's GuideThis PR implements broad frontend feature and UI improvements by consolidating input and image primitives, adding resilient local strategy icons, refining strategy-detail authorization and analysis flows, optimizing assets and layout behavior, and introducing Storybook/Chromatic visual regression tooling. Sequence diagram for resilient image loadingsequenceDiagram
participant Component
participant StrategyIcon
participant SafeImage
participant Browser
participant Placeholder
Component->>StrategyIcon: getIconSource(src, label)
StrategyIcon->>SafeImage: render resolved src
SafeImage->>Browser: load image
alt image loads
Browser-->>SafeImage: onLoad
SafeImage-->>Component: display image
else image fails
Browser-->>SafeImage: onError
SafeImage->>SafeImage: setFailed(true)
SafeImage->>Browser: load fallbackSrc
alt fallback loads
Browser-->>SafeImage: onLoad
SafeImage-->>Component: display fallback image
else fallback fails
Browser-->>SafeImage: onError
SafeImage->>SafeImage: setFallbackFailed(true)
SafeImage-->>Placeholder: render role img placeholder
end
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
안녕하세요. 5개의 이슈를 발견했습니다.
수정된 보안 이슈:
- @remix-run/router (링크)
- axios (링크)
- cross-spawn (링크)
- form-data (링크)
- minimatch (링크)
- path-to-regexp (링크)
- vitest (링크)
AI 에이전트용 프롬프트
이 코드 리뷰의 의견을 반영해 주세요:
## 개별 의견
### 의견 1
<location path="src/pages/strategy/StrategyDetailPage.tsx" line_range="177-178" />
<code_context>
- )
- navigate('/404', { replace: true });
- }, [strategy, isOwner, isAdmin]);
+ if (user) {
+ const isOwnerOrAdmin = isStrategyOwner(strategy, user) || isAdmin(user.role);
+ if (
+ (strategy?.isApproved === 'N' && !isOwnerOrAdmin) ||
</code_context>
<issue_to_address>
**issue (bug_risk):** 초기 로딩 렌더링 중에는 `strategy`가 아직 undefined인데 권한 effect에서 `isStrategyOwner(strategy, user)`를 호출합니다. 따라서 `isStrategyOwner`가 undefined에서 `memberId`를 읽으려 하여 쿼리가 완료되기 전에 전략 상세 페이지가 충돌합니다. 또한 이 호출은 `isStrategyOwner`가 member ID 문자열을 기대하는데 `StrategyDetailProps` 객체를 전달하므로 TypeScript 타입 오류도 발생시킵니다.
**트리거:** 인증된 사용자가 상세 쿼리가 반환되기 전에 전략 상세 페이지를 열 때 발생합니다.
**권장 수정:** `strategy`가 존재하는지 확인하고 `strategy.memberId`를 전달하세요. 예: `strategy && isStrategyOwner(strategy.memberId, user)`.
```suggestion
if (user) {
const isOwnerOrAdmin = strategy && isStrategyOwner(strategy.memberId, user) || isAdmin(user.role);
```
</issue_to_address>
### 의견 2
<location path="src/pages/strategy/StrategyDetailPage.tsx" line_range="177-185" />
<code_context>
- )
- navigate('/404', { replace: true });
- }, [strategy, isOwner, isAdmin]);
+ if (user) {
+ const isOwnerOrAdmin = isStrategyOwner(strategy, user) || isAdmin(user.role);
+ if (
+ (strategy?.isApproved === 'N' && !isOwnerOrAdmin) ||
+ (strategy?.isPosted === 'N' && !isOwnerOrAdmin)
+ )
+ navigate('/404', { replace: true });
+ }
+ }, [strategy, isStrategyOwner, isAdmin]);
+
+ if (isError) {
</code_context>
<issue_to_address>
**🚨 issue (security):** 권한 effect에서 `user`를 읽지만 dependency array에는 포함하지 않았습니다. 따라서 전략 데이터 이후 인증 상태가 로드되어도 effect가 다시 실행되지 않습니다. 인증 상태가 아직 undefined인 동안 리디렉션 검사가 실행되면 비공개 또는 승인되지 않은 전략이 계속 렌더링될 수 있습니다.
**트리거:** 인증 저장소의 hydration이 완료되기 전에 전략 쿼리가 완료될 때 발생합니다.
**권장 수정:** effect dependency array에 `user`를 포함하고, 소유권을 확인하기 전에 `strategy` 값이 존재하는지 확인하세요.
</issue_to_address>
### 의견 3
<location path="src/components/page/strategy-detail/tabmenu/DailyAnalysis.tsx" line_range="344-347" />
<code_context>
};
+ // status prop 변경 시 동기화
useEffect(() => {
setInputStatus(status);
}, [status]);
</code_context>
<issue_to_address>
**issue (broader_impact):** 페이지 변경 시 초기화 로직을 제거하면 사용자가 페이지를 변경해도 `selectedData`와 `selectAll`에 기존 값이 남습니다. 그 결과 일괄 삭제가 이전 페이지에서 선택한 행에 대해 실행될 수 있으며, 새 페이지의 UI에는 여전히 전체 선택 상태가 표시됩니다.
**트리거:** 사용자가 분석 행을 선택한 후 일괄 작업을 실행하기 전에 다른 페이지로 이동할 때 발생합니다.
**권장 수정:** `pagination.currentPage`가 변경될 때마다 `selectedData`와 `selectAll`을 초기화하는 effect를 복원하세요.
</issue_to_address>
### 의견 4
<location path=".github/workflows/chromatic.yml" line_range="3" />
<code_context>
+ projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: comment PR
+ uses: thollander/actions-comment-pull-request@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ message: '🚀storybook: ${{ steps.chromatic.outputs.storybookUrl }}'
</code_context>
<issue_to_address>
**🚨 issue (security):** workflow에서 `GITHUB_TOKEN`에 `pull-requests: write` 권한을 선언하지 않았습니다. 따라서 기본 읽기 전용 토큰을 사용하는 저장소에서는 PR 댓글을 생성하거나 업데이트할 수 없고, Chromatic이 성공하더라도 마지막 단계가 실패합니다.
**트리거:** 저장소의 workflow 토큰이 기본적으로 읽기 전용 권한을 사용할 때 발생합니다.
**권장 수정:** 최상위에 `permissions: pull-requests: write`를 선언하거나 필요한 job 수준 권한을 설정하세요.
```suggestion
on: pull_request
permissions:
pull-requests: write
```
</issue_to_address>
### 의견 5
<location path="index.html" line_range="12" />
<code_context>
<link rel="mask-icon" href="/src/assets/images/favicon/safari-pinned-tab.svg" color="#0D9488" />
<link rel="manifest" href="/src/assets/images/manifest.json" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta
+ property="description"
+ content="트레들리는 체계적이고 효율적인 투자 전략을 제공합니다. 지금 바로 시작해 보세요!"
+ />
<title>트레들리 | 체계적인 투자 전략 플랫폼</title>
</code_context>
<issue_to_address>
**nitpick:** 페이지 설명이 표준 `name="description"` 대신 `property="description"`으로 선언되어 있습니다. 따라서 브라우저와 검색 크롤러가 이를 문서의 meta description으로 인식하지 못합니다.
**권장 수정:** 속성을 `name="description"`으로 변경하세요.
```suggestion
name="description"
```
</issue_to_address>Original comment in English
Hey - I've found 5 issues
Fixed security issues:
- @remix-run/router (link)
- axios (link)
- cross-spawn (link)
- form-data (link)
- minimatch (link)
- path-to-regexp (link)
- vitest (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/pages/strategy/StrategyDetailPage.tsx" line_range="177-178" />
<code_context>
- )
- navigate('/404', { replace: true });
- }, [strategy, isOwner, isAdmin]);
+ if (user) {
+ const isOwnerOrAdmin = isStrategyOwner(strategy, user) || isAdmin(user.role);
+ if (
+ (strategy?.isApproved === 'N' && !isOwnerOrAdmin) ||
</code_context>
<issue_to_address>
**issue (bug_risk):** The permission effect calls `isStrategyOwner(strategy, user)` while `strategy` is still undefined during the initial loading render, so `isStrategyOwner` reads `memberId` from undefined and the strategy detail page crashes before the query completes. This call also passes a `StrategyDetailProps` object where `isStrategyOwner` expects a member ID string, producing a TypeScript type error.
**Triggers:** When an authenticated user opens the strategy detail page before the detail query has returned.
**Suggested fix:** Guard the call with `strategy` and pass `strategy.memberId`, for example `strategy && isStrategyOwner(strategy.memberId, user)`.
```suggestion
if (user) {
const isOwnerOrAdmin = strategy && isStrategyOwner(strategy.memberId, user) || isAdmin(user.role);
```
</issue_to_address>
### Comment 2
<location path="src/pages/strategy/StrategyDetailPage.tsx" line_range="177-185" />
<code_context>
- )
- navigate('/404', { replace: true });
- }, [strategy, isOwner, isAdmin]);
+ if (user) {
+ const isOwnerOrAdmin = isStrategyOwner(strategy, user) || isAdmin(user.role);
+ if (
+ (strategy?.isApproved === 'N' && !isOwnerOrAdmin) ||
+ (strategy?.isPosted === 'N' && !isOwnerOrAdmin)
+ )
+ navigate('/404', { replace: true });
+ }
+ }, [strategy, isStrategyOwner, isAdmin]);
+
+ if (isError) {
</code_context>
<issue_to_address>
**🚨 issue (security):** The authorization effect reads `user` but omits it from its dependency array, so it does not rerun when authentication state loads after the strategy data. A private or unapproved strategy can therefore remain rendered because the redirect check ran while `user` was undefined.
**Triggers:** When the strategy query resolves before the auth store finishes hydrating.
**Suggested fix:** Include `user` in the effect dependency array and guard the strategy value before checking ownership.
</issue_to_address>
### Comment 3
<location path="src/components/page/strategy-detail/tabmenu/DailyAnalysis.tsx" line_range="344-347" />
<code_context>
};
+ // status prop 변경 시 동기화
useEffect(() => {
setInputStatus(status);
}, [status]);
</code_context>
<issue_to_address>
**issue (broader_impact):** Removing the pagination-change reset leaves `selectedData` and `selectAll` populated when the user changes pages. Bulk deletion can therefore operate on rows selected on a previous page, while the UI on the new page still reports the select-all state.
**Triggers:** When a user selects analysis rows and then navigates to another page before using the bulk action.
**Suggested fix:** Restore the effect that clears `selectedData` and `selectAll` whenever `pagination.currentPage` changes.
</issue_to_address>
### Comment 4
<location path=".github/workflows/chromatic.yml" line_range="3" />
<code_context>
+ projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: comment PR
+ uses: thollander/actions-comment-pull-request@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ message: '🚀storybook: ${{ steps.chromatic.outputs.storybookUrl }}'
</code_context>
<issue_to_address>
**🚨 issue (security):** The workflow does not declare `pull-requests: write` permissions for `GITHUB_TOKEN`, so repositories using the default read-only token cannot create or update the PR comment and the final step fails despite Chromatic succeeding.
**Triggers:** When the repository's workflow token defaults to read-only permissions.
**Suggested fix:** Add a top-level `permissions: pull-requests: write` declaration, or configure the required job-level permission.
```suggestion
on: pull_request
permissions:
pull-requests: write
```
</issue_to_address>
### Comment 5
<location path="index.html" line_range="12" />
<code_context>
<link rel="mask-icon" href="/src/assets/images/favicon/safari-pinned-tab.svg" color="#0D9488" />
<link rel="manifest" href="/src/assets/images/manifest.json" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta
+ property="description"
+ content="트레들리는 체계적이고 효율적인 투자 전략을 제공합니다. 지금 바로 시작해 보세요!"
+ />
<title>트레들리 | 체계적인 투자 전략 플랫폼</title>
</code_context>
<issue_to_address>
**nitpick:** The page description is declared with `property="description"` instead of the standard `name="description"`, so browsers and search crawlers do not recognize it as the document meta description.
**Suggested fix:** Change the attribute to `name="description"`.
```suggestion
name="description"
```
</issue_to_address>| if (user) { | ||
| const isOwnerOrAdmin = isStrategyOwner(strategy, user) || isAdmin(user.role); |
There was a problem hiding this comment.
issue (bug_risk): 초기 로딩 렌더링 중에는 strategy가 아직 undefined인데 권한 effect에서 isStrategyOwner(strategy, user)를 호출합니다. 따라서 isStrategyOwner가 undefined에서 memberId를 읽으려 하여 쿼리가 완료되기 전에 전략 상세 페이지가 충돌합니다. 또한 이 호출은 isStrategyOwner가 member ID 문자열을 기대하는데 StrategyDetailProps 객체를 전달하므로 TypeScript 타입 오류도 발생시킵니다.
트리거: 인증된 사용자가 상세 쿼리가 반환되기 전에 전략 상세 페이지를 열 때 발생합니다.
권장 수정: strategy가 존재하는지 확인하고 strategy.memberId를 전달하세요. 예: strategy && isStrategyOwner(strategy.memberId, user).
| if (user) { | |
| const isOwnerOrAdmin = isStrategyOwner(strategy, user) || isAdmin(user.role); | |
| if (user) { | |
| const isOwnerOrAdmin = strategy && isStrategyOwner(strategy.memberId, user) || isAdmin(user.role); |
Original comment in English
issue (bug_risk): The permission effect calls isStrategyOwner(strategy, user) while strategy is still undefined during the initial loading render, so isStrategyOwner reads memberId from undefined and the strategy detail page crashes before the query completes. This call also passes a StrategyDetailProps object where isStrategyOwner expects a member ID string, producing a TypeScript type error.
Triggers: When an authenticated user opens the strategy detail page before the detail query has returned.
Suggested fix: Guard the call with strategy and pass strategy.memberId, for example strategy && isStrategyOwner(strategy.memberId, user).
| if (user) { | |
| const isOwnerOrAdmin = isStrategyOwner(strategy, user) || isAdmin(user.role); | |
| if (user) { | |
| const isOwnerOrAdmin = strategy && isStrategyOwner(strategy.memberId, user) || isAdmin(user.role); |
| if (user) { | ||
| const isOwnerOrAdmin = isStrategyOwner(strategy, user) || isAdmin(user.role); | ||
| if ( | ||
| (strategy?.isApproved === 'N' && !isOwnerOrAdmin) || | ||
| (strategy?.isPosted === 'N' && !isOwnerOrAdmin) | ||
| ) | ||
| navigate('/404', { replace: true }); | ||
| } | ||
| }, [strategy, isStrategyOwner, isAdmin]); |
There was a problem hiding this comment.
🚨 issue (security): 권한 effect에서 user를 읽지만 dependency array에는 포함하지 않았습니다. 따라서 전략 데이터 이후 인증 상태가 로드되어도 effect가 다시 실행되지 않습니다. 인증 상태가 아직 undefined인 동안 리디렉션 검사가 실행되면 비공개 또는 승인되지 않은 전략이 계속 렌더링될 수 있습니다.
트리거: 인증 저장소의 hydration이 완료되기 전에 전략 쿼리가 완료될 때 발생합니다.
권장 수정: effect dependency array에 user를 포함하고, 소유권을 확인하기 전에 strategy 값이 존재하는지 확인하세요.
Original comment in English
🚨 issue (security): The authorization effect reads user but omits it from its dependency array, so it does not rerun when authentication state loads after the strategy data. A private or unapproved strategy can therefore remain rendered because the redirect check ran while user was undefined.
Triggers: When the strategy query resolves before the auth store finishes hydrating.
Suggested fix: Include user in the effect dependency array and guard the strategy value before checking ownership.
| useEffect(() => { | ||
| setSelectedData([]); | ||
| setSelectAll(false); | ||
| }, [pagination.currentPage]); |
There was a problem hiding this comment.
issue (broader_impact): 페이지 변경 시 초기화 로직을 제거하면 사용자가 페이지를 변경해도 selectedData와 selectAll에 기존 값이 남습니다. 그 결과 일괄 삭제가 이전 페이지에서 선택한 행에 대해 실행될 수 있으며, 새 페이지의 UI에는 여전히 전체 선택 상태가 표시됩니다.
트리거: 사용자가 분석 행을 선택한 후 일괄 작업을 실행하기 전에 다른 페이지로 이동할 때 발생합니다.
권장 수정: pagination.currentPage가 변경될 때마다 selectedData와 selectAll을 초기화하는 effect를 복원하세요.
Original comment in English
issue (broader_impact): Removing the pagination-change reset leaves selectedData and selectAll populated when the user changes pages. Bulk deletion can therefore operate on rows selected on a previous page, while the UI on the new page still reports the select-all state.
Triggers: When a user selects analysis rows and then navigates to another page before using the bulk action.
Suggested fix: Restore the effect that clears selectedData and selectAll whenever pagination.currentPage changes.
| @@ -0,0 +1,27 @@ | |||
| name: 'Chromatic Deployment' | |||
|
|
|||
| on: pull_request | |||
There was a problem hiding this comment.
🚨 issue (security): workflow에서 GITHUB_TOKEN에 pull-requests: write 권한을 선언하지 않았습니다. 따라서 기본 읽기 전용 토큰을 사용하는 저장소에서는 PR 댓글을 생성하거나 업데이트할 수 없고, Chromatic이 성공하더라도 마지막 단계가 실패합니다.
트리거: 저장소의 workflow 토큰이 기본적으로 읽기 전용 권한을 사용할 때 발생합니다.
권장 수정: 최상위에 permissions: pull-requests: write를 선언하거나 필요한 job 수준 권한을 설정하세요.
| on: pull_request | |
| on: pull_request | |
| permissions: | |
| pull-requests: write |
Original comment in English
🚨 issue (security): The workflow does not declare pull-requests: write permissions for GITHUB_TOKEN, so repositories using the default read-only token cannot create or update the PR comment and the final step fails despite Chromatic succeeding.
Triggers: When the repository's workflow token defaults to read-only permissions.
Suggested fix: Add a top-level permissions: pull-requests: write declaration, or configure the required job-level permission.
| on: pull_request | |
| on: pull_request | |
| permissions: | |
| pull-requests: write |
| <link rel="manifest" href="/src/assets/images/manifest.json" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <meta | ||
| property="description" |
There was a problem hiding this comment.
nitpick: 페이지 설명이 표준 name="description" 대신 property="description"으로 선언되어 있습니다. 따라서 브라우저와 검색 크롤러가 이를 문서의 meta description으로 인식하지 못합니다.
권장 수정: 속성을 name="description"으로 변경하세요.
| property="description" | |
| name="description" |
Original comment in English
nitpick: The page description is declared with property="description" instead of the standard name="description", so browsers and search crawlers do not recognize it as the document meta description.
Suggested fix: Change the attribute to name="description".
| property="description" | |
| name="description" |
🚀 풀 리퀘스트 제안
📋 작업 내용
수정한 내용이나 추가한 기능에 대해 자세히 설명해 주세요.
🔧 변경 사항
주요 변경 사항을 요약해 주세요.
📸 스크린샷 (선택 사항)
수정된 화면 또는 기능을 시연할 수 있는 스크린샷을 첨부해 주세요.
📄 기타
추가적으로 전달하고 싶은 내용이나 특별한 요구 사항이 있으면 작성해 주세요.
Sourcery 요약
프론트엔드의 핵심 UI 개선, 안정적인 에셋 처리, 재사용 가능한 폼 컴포넌트, 전략 표시 업데이트 및 시각적 검토 워크플로를 구현합니다.
새로운 기능:
버그 수정:
개선 사항:
빌드:
CI:
테스트:
정리:
Original summary in English
Summary by Sourcery
Implement the frontend’s core UI improvements, resilient asset handling, reusable form components, strategy presentation updates, and visual review workflow.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Tests:
Chores: