-
Notifications
You must be signed in to change notification settings - Fork 3
[feature] 사용자가 지원하기를 눌렀을때 지원가능한 지원서들이 모달로 뜬다 #787
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
058f4a3
7c94b70
7dcb97b
fef1c3e
d762de6
8bba6d7
87d1757
4440bb8
01d9bc3
62aeef4
53ff2b3
40a5556
402e906
97cd642
df2de81
a5f1853
e52fee6
73f4db1
ab9d09b
92221f4
f390eb0
66eea33
c3c6d4a
919bf61
b3d860e
7fd2b26
e7bfaa1
4108f88
c6709b0
0d39d6b
ca1462a
ad58cc7
f446b1b
c159a23
428b637
bca239a
bba7f92
ce3f273
e642dac
a70dbbf
2014acb
e20db39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import API_BASE_URL from "@/constants/api"; | ||
|
|
||
| const getApplicationOptions = async (clubId: string) => { | ||
| try { | ||
| const response = await fetch(`${API_BASE_URL}/api/club/${clubId}/apply`); | ||
| if (!response.ok) { | ||
| let message = response.statusText; | ||
| try { | ||
| const errorData = await response.json(); | ||
| if (errorData?.message) message = errorData.message; | ||
| } catch {} | ||
| console.error(`Failed to fetch options: ${message}`); | ||
| throw new Error(message); | ||
| } | ||
|
|
||
| const result = await response.json(); | ||
| let forms: Array<{ id: string; title: string }> = []; | ||
| if (result && result.data && Array.isArray(result.data.forms)) { | ||
| forms = result.data.forms; | ||
| } | ||
| return forms; | ||
| } catch (error) { | ||
| console.error('지원서 옵션 조회 중 오류가 발생했습니다.', error); | ||
| throw error; | ||
| } | ||
| }; | ||
|
|
||
| export default getApplicationOptions; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import styled from 'styled-components'; | ||
|
|
||
| export const EmptyMessage = styled.div` | ||
| padding: 16px 8px; | ||
| color: #9D9D9D; | ||
| text-align: center; | ||
| font-weight: 600; | ||
| `; | ||
|
Comment on lines
+3
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 매직 컬러 값을 상수로 추출하세요. Line 5의 색상 테마 파일을 생성하거나 기존 테마 파일을 사용하세요: // theme/colors.ts 또는 유사한 파일에서
export const COLORS = {
textSecondary: '#9D9D9D',
// ...
} as const;그런 다음 스타일에서 사용: +import { COLORS } from '@/theme/colors';
+
export const EmptyMessage = styled.div`
padding: 16px 8px;
- color: #9D9D9D;
+ color: ${COLORS.textSecondary};
text-align: center;
font-weight: 600;
`;🤖 Prompt for AI Agents |
||
|
|
||
| export const List = styled.div` | ||
| display: grid; | ||
| gap: 16px; | ||
| `; | ||
|
|
||
| export const OptionButton = styled.button` | ||
| width: 100%; | ||
| padding: 18px 20px; | ||
| border-radius: 10px; | ||
| border: 1px solid #DCDCDC; | ||
| background: #fff; | ||
| font-weight: 600; | ||
| font-size: 16px; | ||
| cursor: pointer; | ||
| transition: background-color .15s ease, color .15s ease, border-color .15s ease; | ||
|
|
||
| &:hover { | ||
| background: #ff7a00; | ||
| color: #fff; | ||
| border-color: #ff7a00; | ||
| } | ||
| `; | ||
|
Comment on lines
+15
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 매직 컬러 값을 상수로 추출하세요. Lines 19, 27, 29에 하드코딩된 색상 값들( 색상 테마 파일을 생성하거나 기존 테마 파일을 사용하세요: // theme/colors.ts 또는 유사한 파일에서
export const COLORS = {
borderGray: '#DCDCDC',
primary: '#ff7a00',
white: '#fff',
// ...
} as const;그런 다음 스타일에서 사용: +import { COLORS } from '@/theme/colors';
+
export const OptionButton = styled.button`
width: 100%;
padding: 18px 20px;
border-radius: 10px;
- border: 1px solid #DCDCDC;
- background: #fff;
+ border: 1px solid ${COLORS.borderGray};
+ background: ${COLORS.white};
font-weight: 600;
font-size: 16px;
cursor: pointer;
transition: background-color .15s ease, color .15s ease, border-color .15s ease;
&:hover {
- background: #ff7a00;
- color: #fff;
- border-color: #ff7a00;
+ background: ${COLORS.primary};
+ color: ${COLORS.white};
+ border-color: ${COLORS.primary};
}
`;🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import Modal from "@/components/common/Modal/Modal"; | ||
| import * as Styled from './ApplicationSelectModal.styles'; | ||
| import { ApplicationForm } from "@/types/application"; | ||
|
|
||
| export interface ApplicationSelectModalProps { | ||
| isOpen: boolean; | ||
| onClose: () => void; | ||
| options: ApplicationForm[]; | ||
| onSelect: (option: ApplicationForm) => void; | ||
| onBackdropClick?: () => boolean | void; | ||
| } | ||
|
|
||
| interface OptionsListProps { | ||
| options: ApplicationForm[]; | ||
| onSelect: (option: ApplicationForm) => void; | ||
| } | ||
|
|
||
| const OptionsList = ({ options, onSelect }: OptionsListProps) => { | ||
| if (options.length === 0) { | ||
| return <Styled.EmptyMessage>지원 가능한 분야가 없습니다.</Styled.EmptyMessage>; | ||
| } | ||
|
|
||
| return ( | ||
| <Styled.List> | ||
| {options.map((option) => ( | ||
| <Styled.OptionButton key={option.id} onClick={() => {onSelect(option);}}> | ||
| {option.title} | ||
| </Styled.OptionButton> | ||
| ))} | ||
| </Styled.List> | ||
| ) | ||
| }; | ||
|
|
||
| const ApplicationSelectModal = ({ isOpen, onClose, options, onSelect, onBackdropClick }: ApplicationSelectModalProps) => { | ||
| const handleOverlayClick = () => { | ||
| return false; | ||
| }; | ||
|
|
||
| return ( | ||
| <Modal | ||
| isOpen={isOpen} | ||
| onClose={onClose} | ||
| title="지원 분야 선택" | ||
| description="지원할 분야를 선택해주세요" | ||
| onBackdropClick={handleOverlayClick} | ||
| > | ||
| <OptionsList options={options} onSelect={onSelect} /> | ||
| </Modal> | ||
| ); | ||
| }; | ||
|
|
||
| export default ApplicationSelectModal; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { media } from '@/styles/mediaQuery'; | ||
| import styled from 'styled-components'; | ||
|
|
||
| export const Overlay = styled.div<{ isOpen: boolean }>` | ||
| position: fixed; | ||
| inset: 0; | ||
| z-index: 1000; | ||
| background: rgba(0,0,0, ${({ isOpen }) => (isOpen ? 0.45 : 0)}); | ||
| display: grid; | ||
| place-items: center; | ||
| padding: 24px; | ||
| transition: background-color .2s ease; | ||
| `; | ||
|
|
||
| export const Container = styled.div<{ isOpen: boolean }>` | ||
| max-width: 500px; | ||
| width: 100%; | ||
| max-height: 90vh; | ||
| background: #fff; | ||
| border-radius: 10px; | ||
| overflow: hidden; | ||
| box-shadow: ${({ isOpen }) => (isOpen ? '0 18px 44px rgba(0,0,0,.22)' : 'none')}; | ||
| transition: transform .2s ease, box-shadow .2s ease; | ||
| `; | ||
|
|
||
| export const Header = styled.div` | ||
| padding: 30px; | ||
| border-bottom: 1px solid #DCDCDC; | ||
| display: flex; | ||
| align-items: center; | ||
| `; | ||
|
|
||
| export const Title = styled.h3` | ||
| font-size: 20px; | ||
| font-weight: 800; | ||
| flex: 1; | ||
| text-align: left; | ||
| `; | ||
|
|
||
| export const IconButton = styled.button` | ||
| border: none; | ||
| background: transparent; | ||
| font-size: 20px; | ||
| font-weight: 800; | ||
| color: #9D9D9D; | ||
| line-height: 1; | ||
| cursor: pointer; | ||
| `; | ||
|
|
||
| export const Description = styled.p` | ||
| padding: 20px 32px 0px; | ||
| text-align: left; | ||
| color: #9D9D9D; | ||
| font-weight: 600; | ||
| `; | ||
|
|
||
| export const Body = styled.div` | ||
| padding: 16px 30px 30px; | ||
| overflow: auto; | ||
| `; |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 모달 컴포넌트 생성 수고하셨습니다.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 공통 컴포넌트를 만드는 것에는 생각보다 많은 고민이 필요한 것 같습니다. 현재 모달을 더 좋은 방향으로 변경해도 좋을 것 같네요! |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,40 @@ | ||||||||||||||||||||||||||||||||||
| import { MouseEvent, ReactNode, useEffect } from "react"; | ||||||||||||||||||||||||||||||||||
| import * as Styled from './Modal.styles'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export interface ModalProps { | ||||||||||||||||||||||||||||||||||
| isOpen: boolean; | ||||||||||||||||||||||||||||||||||
| onClose: () => void; | ||||||||||||||||||||||||||||||||||
| title: string; | ||||||||||||||||||||||||||||||||||
| description?: string; | ||||||||||||||||||||||||||||||||||
| children?: ReactNode; | ||||||||||||||||||||||||||||||||||
| onBackdropClick?: () => boolean | void; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
4
to
11
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 바깥쪽을 클릭했을 때의 핸들러 또한 추가하면 어떨까요? 모달에서 바깥쪽을 클릭했을 때의 동작도 각 모달마다 다를 수 있다고 생각합니다!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
+4
to
+11
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
다음 중 하나를 적용하세요: 옵션 1: title을 선택적 속성으로 변경 - title: string;
+ title?: string;옵션 2: 조건부 렌더링 제거 (title이 항상 제공된다면) - {title && <Styled.Title>{title}</Styled.Title>}
+ <Styled.Title>{title}</Styled.Title>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const Modal = ({ isOpen, onClose, title, description, children, onBackdropClick }: ModalProps) => { | ||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||
| if (isOpen) { | ||||||||||||||||||||||||||||||||||
| document.body.style.overflow = 'hidden'; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||
| document.body.style.overflow = ''; | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| }, [isOpen]); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (!isOpen) return null; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <Styled.Overlay isOpen={isOpen} onClick={onBackdropClick} aria-modal="true"> | ||||||||||||||||||||||||||||||||||
| <Styled.Container isOpen={isOpen} onClick={(e: MouseEvent<HTMLDivElement>) => e.stopPropagation()}> | ||||||||||||||||||||||||||||||||||
| <Styled.Header> | ||||||||||||||||||||||||||||||||||
| {title && <Styled.Title>{title}</Styled.Title>} | ||||||||||||||||||||||||||||||||||
| <Styled.IconButton aria-label="close" type="button" onClick={onClose}>✕</Styled.IconButton> | ||||||||||||||||||||||||||||||||||
| </Styled.Header> | ||||||||||||||||||||||||||||||||||
| {description && <Styled.Description>{description}</Styled.Description>} | ||||||||||||||||||||||||||||||||||
| <Styled.Body>{children}</Styled.Body> | ||||||||||||||||||||||||||||||||||
| </Styled.Container> | ||||||||||||||||||||||||||||||||||
| </Styled.Overlay> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export default Modal; | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,11 @@ | ||
| import { useQuery } from '@tanstack/react-query'; | ||
| import getApplication from '@/apis/application/getApplication'; | ||
|
|
||
| export const useGetApplication = (clubId: string) => { | ||
| export const useGetApplication = (clubId?: string | null, applicationFormId?: string | null) => { | ||
| return useQuery({ | ||
| queryKey: ['applicationForm', clubId], | ||
| queryFn: () => getApplication(clubId), | ||
| queryKey: ['applicationForm', clubId, applicationFormId], | ||
| queryFn: () => getApplication(clubId!, applicationFormId!), | ||
| retry: false, | ||
| enabled: !!clubId, | ||
| enabled: !!clubId && !!applicationFormId, | ||
| }); | ||
| }; |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 목 데이터 써주신거 좋네요~ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { http, HttpResponse } from 'msw'; | ||
| import { mockData } from '../data/mockData'; | ||
| import { mockData, mockOptions } from '../data/mockData'; | ||
| import { API_BASE } from '../constants/clubApi'; | ||
| import { validateClubId } from '../utils/validateClubId'; | ||
| import { ERROR_MESSAGE } from '../constants/error'; | ||
|
|
@@ -24,9 +24,14 @@ export const applyHandlers = [ | |
|
|
||
| return HttpResponse.json( | ||
| { | ||
| clubId, | ||
| form_title: mockData.title, | ||
| questions: mockData.questions, | ||
| status: '200', | ||
| message: 'OK', | ||
| data: { | ||
| clubId, | ||
| title: mockData.title, | ||
| description: mockData.description, | ||
| questions: mockData.questions, | ||
| }, | ||
| }, | ||
| { status: 200 }, | ||
| ); | ||
|
|
@@ -69,4 +74,16 @@ export const applyHandlers = [ | |
| { status: 200 }, | ||
| ); | ||
| }), | ||
|
|
||
| http.get(`${API_BASE}/:clubId/applications`, ({ params }) => { | ||
| const clubId = String(params.clubId); | ||
| if (!validateClubId(clubId)) { | ||
| return HttpResponse.json( | ||
| { message: ERROR_MESSAGE.INVALID_CLUB_ID }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
| const list = mockOptions[clubId] ?? []; | ||
| return HttpResponse.json({data: list}, {status: 200}); | ||
| }), | ||
|
Comment on lines
+78
to
+88
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| ]; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,8 @@ | ||
| export const API_BASE = 'http://localhost/api/club'; | ||
|
|
||
| export const CLUB_ID = '67e54ae51cfd27718dd40be6'; | ||
|
|
||
| export const CLUB_IVF = '67ee2ca3b35e3c267e3c248d'; | ||
| export const CLUB_BOB = '67e54ae51cfd27718dd40bea'; | ||
| export const CLUB_BACK = '67e54ae51cfd27718dd40bd8'; | ||
| export const CLUB_TEST = '67ebf9f75c8623081055881c'; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -38,9 +38,9 @@ const ApplicantDetailPage = () => { | |||||||||||||||||||||||||
| const [applicantStatus, setApplicantStatus] = useState<ApplicationStatus>( | ||||||||||||||||||||||||||
| ApplicationStatus.SUBMITTED, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| const { applicantsData, clubId } = useAdminClubContext(); | ||||||||||||||||||||||||||
| const { applicantsData, clubId, applicationFormId } = useAdminClubContext(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const { data: formData, isLoading, isError } = useGetApplication(clubId!); | ||||||||||||||||||||||||||
| const { data: formData, isLoading, isError } = useGetApplication(clubId!, applicationFormId!); | ||||||||||||||||||||||||||
| const { mutate: updateApplicant } = useUpdateApplicant(clubId!); | ||||||||||||||||||||||||||
|
Comment on lines
+41
to
44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-null assertion 사용으로 인한 잠재적 런타임 에러를 방지하세요. Line 43에서 다음 diff를 적용하여 안전하게 처리하세요: - const { applicantsData, clubId, applicationFormId } = useAdminClubContext();
-
- const { data: formData, isLoading, isError } = useGetApplication(clubId!, applicationFormId!);
- const { mutate: updateApplicant } = useUpdateApplicant(clubId!);
+ const { applicantsData, clubId, applicationFormId } = useAdminClubContext();
+
+ const { data: formData, isLoading, isError } = useGetApplication(
+ clubId || '',
+ applicationFormId || ''
+ );
+ const { mutate: updateApplicant } = useUpdateApplicant(clubId || '');또는 early return으로 guard를 추가하세요: const { applicantsData, clubId, applicationFormId } = useAdminClubContext();
+
+ if (!clubId || !applicationFormId) {
+ return <div>클럽 정보를 불러올 수 없습니다.</div>;
+ }
- const { data: formData, isLoading, isError } = useGetApplication(clubId!, applicationFormId!);
+ const { data: formData, isLoading, isError } = useGetApplication(clubId, applicationFormId);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const applicantIndex = | ||||||||||||||||||||||||||
|
|
@@ -75,7 +75,7 @@ const ApplicantDetailPage = () => { | |||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||
| }, 400), | ||||||||||||||||||||||||||
| [clubId, questionId], | ||||||||||||||||||||||||||
| [clubId, questionId, updateApplicant], | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!applicantsData) { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api마다 매번 try/catch를 하고, 또 에러를 던지는 식으로 구현을 해 왔는데,
이러면 작성해야 할 코드양도 많아지고 에러를 어디서 받는지 알 수 없더라고요.
discussion에서 api 구조에 대해 같이 얘기해보면 좋을 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은 피드백 감사합니다! 말씀하신 부분 공감돼요.
구현할때, 어떻게 하는게 좋을지 몰라서 기존 방식을 최대한 따라갔거든요.
API마다 try/catch로 처리하다 보니 중복 코드가 많고, 에러 핸들링 위치도 애매한 것 같더라구요.
저도 공통 에러 핸들링 로직이나 API 유틸을 따로 만들어서 관리하는 방식으로 개선하면 좋을 것 같아요. discussion에서 구조를 같이 정리해보면 좋겠네요!