Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/mocks/repositories/applicant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const applicants: ApplicantData[] = [
department: '소프트웨어공학과',
phoneNumber: '010-1010-1010',
email: 'ddd@naver.com',
status: '미정',
status: 'PENDING',
},
{
id: 2,
Expand All @@ -18,7 +18,7 @@ const applicants: ApplicantData[] = [
department: '소프트웨어공학과',
phoneNumber: '010-1010-1010',
email: 'ddd@naver.com',
status: '합격',
status: 'APPROVED',
},
{
id: 3,
Expand All @@ -27,7 +27,7 @@ const applicants: ApplicantData[] = [
department: '소프트웨어공학과',
phoneNumber: '010-1010-1010',
email: 'ddd@naver.com',
status: '불합격',
status: 'REJECTED',
},
{
id: 4,
Expand All @@ -36,7 +36,7 @@ const applicants: ApplicantData[] = [
department: '소프트웨어공학과',
phoneNumber: '010-1010-1010',
email: 'ddd@naver.com',
status: '미정',
status: 'PENDING',
},
];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import styled from '@emotion/styled';
import type { ApplicantData, ApplicantStatus } from '@/pages/admin/Dashboard/types/dashboard';
import type { StatusLabel, ApplicationStatus } from '@/pages/admin/Dashboard/types/dashboard';

type Props = {
label: ApplicantData['status'];
value: ApplicantStatus;
label: StatusLabel;
value: ApplicationStatus;
selected: boolean;
onClick: (status: ApplicantStatus) => void;
onClick: (status: ApplicationStatus) => void;
};

export const ApplicantStatusButton = ({ label, value, selected, onClick }: Props) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import { ApplicantStatusButton } from './ApplicantStatusButton';
import type { ApplicantStatus } from '@/pages/admin/Dashboard/types/dashboard';
import type { ApplicationStatus } from '@/pages/admin/Dashboard/types/dashboard';

type Props = {
status?: ApplicantStatus;
updateStatus: (status: ApplicantStatus) => void;
status?: ApplicationStatus;
updateStatus: (status: ApplicationStatus) => void;
};

export const ApplicantStatusToggle = ({ status, updateStatus }: Props) => {
const [statusOption, setStatusOption] = useState(status);

const handleClick = (newStatus: ApplicantStatus) => {
const handleClick = (newStatus: ApplicationStatus) => {
setStatusOption(newStatus);
updateStatus(newStatus);
};
Expand All @@ -20,8 +20,8 @@ export const ApplicantStatusToggle = ({ status, updateStatus }: Props) => {
<Container>
<ApplicantStatusButton
label={'합격'}
value={'ACCEPTED'}
selected={statusOption === 'ACCEPTED'}
value={'APPROVED'}
selected={statusOption === 'APPROVED'}
onClick={handleClick}
/>
<ApplicantStatusButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import styled from '@emotion/styled';
import { Text } from '@/shared/components/Text';
import { ApplicantStarRating } from './ApplicantStarRating';
import { ApplicantStatusToggle } from './ApplicantStatusToggle';
import type { ApplicantStatus } from '@/pages/admin/Dashboard/types/dashboard';
import type { ApplicationStatus } from '@/pages/admin/Dashboard/types/dashboard';

type Props = {
name?: string;
department?: string;
status?: ApplicantStatus;
status?: ApplicationStatus;
rating?: number;
updateStatus: (status: ApplicantStatus) => void;
updateStatus: (status: ApplicationStatus) => void;
};

export const ApplicantProfileSection = ({
Expand Down
4 changes: 2 additions & 2 deletions src/pages/admin/ApplicationDetail/types/detailApplication.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { ApplicantStatus } from '@/pages/admin/Dashboard/types/dashboard';
import type { ApplicationStatus } from '@/pages/admin/Dashboard/types/dashboard';

export type DetailApplication = {
applicationId: number;
status: ApplicantStatus;
status: ApplicationStatus;
rating: number;
applicantInfo: {
applicantId: number;
Expand Down
11 changes: 11 additions & 0 deletions src/pages/admin/Dashboard/api/dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { DashboardSummary } from '@/pages/admin/Dashboard/types/dashboard';

export const fetchDashboardSummary = async (clubId: number): Promise<DashboardSummary> => {
const response = await fetch(`/api/clubs/${clubId}/dashboard`);

if (!response.ok) {
throw new Error('대시보드 요약 정보를 불러오는 데 실패했습니다.');
}
Comment on lines +6 to +8
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Improve error handling with HTTP status information.

The generic error message provides no debugging context. Including the HTTP status code and response details would significantly improve error diagnostics and help differentiate between client errors (4xx) and server errors (5xx).

Apply this diff to enhance error reporting:

 if (!response.ok) {
-  throw new Error('대시보드 요약 정보를 불러오는 데 실패했습니다.');
+  const errorMessage = `대시보드 요약 정보를 불러오는 데 실패했습니다. (상태 코드: ${response.status})`;
+  throw new Error(errorMessage);
 }

Alternatively, for more comprehensive error handling:

 if (!response.ok) {
-  throw new Error('대시보드 요약 정보를 불러오는 데 실패했습니다.');
+  let errorMessage = `대시보드 요약 정보를 불러오는 데 실패했습니다. (상태 코드: ${response.status})`;
+  try {
+    const errorBody = await response.json();
+    if (errorBody?.message) {
+      errorMessage += ` - ${errorBody.message}`;
+    }
+  } catch {
+    // Ignore JSON parsing errors for error responses
+  }
+  throw new Error(errorMessage);
 }
📝 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
if (!response.ok) {
throw new Error('대시보드 요약 정보를 불러오는 데 실패했습니다.');
}
if (!response.ok) {
let errorMessage = `대시보드 요약 정보를 불러오는 데 실패했습니다. (상태 코드: ${response.status})`;
try {
const errorBody = await response.json();
if (errorBody?.message) {
errorMessage += ` - ${errorBody.message}`;
}
} catch {
// Ignore JSON parsing errors for error responses
}
throw new Error(errorMessage);
}
🤖 Prompt for AI Agents
In src/pages/admin/Dashboard/api/dashboard.ts around lines 6 to 8, the current
error throw uses a generic message; change it to include HTTP status and
response details by reading response.status and response.statusText (and
optionally await response.text() or response.json() safely) and include those
values in the thrown Error (or processLogger.error) so the message contains
status code, status text and a short response body snippet for debugging.


return response.json();
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,33 @@ export const ApplicantList = ({ filterOption }: Props) => {
))}
</ApplicantInfoCategoryList>
<ApplicantInfoDataList>
{applicants.map((applicant) => (
<ApplicantListItem
key={applicant.id}
id={applicant.id}
name={applicant.name}
studentId={applicant.studentId}
department={applicant.department}
phoneNumber={applicant.phoneNumber}
email={applicant.email}
status={applicant.status}
onClick={handleItemClick}
/>
))}
{applicants.length > 0 ? (
applicants.map((applicant) => (
<ApplicantListItem
key={applicant.id}
id={applicant.id}
name={applicant.name}
studentId={applicant.studentId}
department={applicant.department}
phoneNumber={applicant.phoneNumber}
email={applicant.email}
status={applicant.status}
onClick={handleItemClick}
/>
))
) : (
<EmptyMessage>
{filterOption === 'ALL'
? '아직 지원자가 없습니다.'
: `${filterOption === 'PENDING' ? '심사중' : filterOption === 'APPROVED' ? '합격' : '불합격'} 지원자가 없습니다.`}
Comment on lines +50 to +52
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

중첩된 삼항 연산자를 사용하여 빈 목록 메시지를 생성하는 것은 가독성을 저해하고 유지보수를 어렵게 만듭니다. 상태 값에 따른 표시 문자열을 객체로 매핑하여 사용하면 코드가 더 명확해지고 관리하기 쉬워집니다.

예를 들어, 컴포넌트 상단에 다음과 같은 맵을 정의하고 사용할 수 있습니다.

const statusLabels = {
  PENDING: '심사중',
  APPROVED: '합격',
  REJECTED: '불합격',
};

// JSX 내부
{
  filterOption === 'ALL'
    ? '아직 지원자가 없습니다.'
    : `${statusLabels[filterOption]} 지원자가 없습니다.`
}

</EmptyMessage>
)}
</ApplicantInfoDataList>
<ButtonWrapper>
<Button width={'15rem'}>결과 전송하기</Button>
</ButtonWrapper>
{applicants.length > 0 && (
<ButtonWrapper>
<Button width={'15rem'}>결과 전송하기</Button>
</ButtonWrapper>
)}
</Container>
);
};
Expand Down Expand Up @@ -82,6 +92,13 @@ const ButtonWrapper = styled.div({
width: '100%',
});

const EmptyMessage = styled.div(({ theme }) => ({
padding: '4rem',
textAlign: 'center',
color: theme.colors.gray500,
fontSize: '1.4rem',
}));

type ApplicateInfoCategory = '이름' | '학번' | '학과' | '전화번호' | '이메일' | '결과';
const INFO_CATEGORY: ApplicateInfoCategory[] = [
'이름',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ type Props = ApplicantData & {
onClick: (id: number) => void;
};

const STATUS_LABEL: Record<ApplicantData['status'], string> = {
PENDING: '미정',
REJECTED: '불합격',
APPROVED: '합격',
};

export const ApplicantListItem = ({
id,
name,
Expand All @@ -22,7 +28,7 @@ export const ApplicantListItem = ({
<InfoText>{department || '-'}</InfoText>
<InfoText>{phoneNumber || '-'}</InfoText>
<InfoText>{email || '-'}</InfoText>
<StatusBadge status={status}>{status || '-'}</StatusBadge>
<StatusBadge status={status}>{STATUS_LABEL[status] || '-'}</StatusBadge>
</ItemWrapper>
);
};
Expand All @@ -47,15 +53,15 @@ const InfoText = styled.p(({ theme }) => ({

const StatusBadge = styled.p<Pick<ApplicantData, 'status'>>(({ theme, status }) => {
const styles = {
합격: {
APPROVED: {
backgroundColor: theme.colors.primary100,
color: theme.colors.primary800,
},
불합격: {
REJECTED: {
backgroundColor: theme.colors.red100,
color: theme.colors.red600,
},
미정: {
PENDING: {
backgroundColor: theme.colors.gray100,
color: theme.colors.gray600,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { useApplicants } from '@/pages/admin/Dashboard/hooks/useApplicants';
import { ApplicantFilterButton } from './ApplicationFilterButton';
import type { ApplicationFilterOption } from '@/pages/admin/Dashboard/types/dashboard';

Expand All @@ -8,29 +9,31 @@ export type Props = {
};

export const ApplicationStatusFilter = ({ option, onOptionChange }: Props) => {
const { counts } = useApplicants(1);
Copy link
Contributor

Choose a reason for hiding this comment

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

high

clubId1로 하드코딩되어 있습니다. 이 컴포넌트가 다른 클럽에서도 재사용될 수 있도록 clubId를 props로 받거나 URL 파라미터에서 동적으로 가져오는 방식으로 수정해야 합니다.

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove hardcoded clubId.

The hardcoded clubId = 1 is a magic number that breaks modularity and will cause issues when viewing different clubs' dashboards.

Pass clubId as a prop:

 export type Props = {
+  clubId: number;
   option: ApplicationFilterOption;
   onOptionChange: (option: ApplicationFilterOption) => void;
 };

-export const ApplicationStatusFilter = ({ option, onOptionChange }: Props) => {
-  const { counts } = useApplicants(1);
+export const ApplicationStatusFilter = ({ clubId, option, onOptionChange }: Props) => {
+  const { counts } = useApplicants(clubId);
📝 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
const { counts } = useApplicants(1);
export type Props = {
clubId: number;
option: ApplicationFilterOption;
onOptionChange: (option: ApplicationFilterOption) => void;
};
export const ApplicationStatusFilter = ({ clubId, option, onOptionChange }: Props) => {
const { counts } = useApplicants(clubId);
🤖 Prompt for AI Agents
In
src/pages/admin/Dashboard/components/ApplicantListSection/ApplicationFilter.tsx
around line 12 the hook is called with a hardcoded clubId (useApplicants(1));
replace the magic number by accepting a clubId prop on ApplicationFilter (with
appropriate typing), use that prop in the useApplicants call (e.g.,
useApplicants(clubId)), and update the parent(s) that render ApplicationFilter
to pass the correct clubId down (ensure any tests/types are updated
accordingly).


return (
<Wrapper>
<ApplicantFilterButton
value={'ALL'}
label={'전체'}
label={`전체 (${counts.ALL})`}
selected={option === 'ALL'}
onClick={onOptionChange}
/>
<ApplicantFilterButton
value={'ACCEPTED'}
label={'합격'}
selected={option === 'ACCEPTED'}
value={'APPROVED'}
label={`합격 (${counts.APPROVED})`}
selected={option === 'APPROVED'}
onClick={onOptionChange}
/>
<ApplicantFilterButton
value={'REJECTED'}
label={'불합격'}
label={`불합격 (${counts.REJECTED})`}
selected={option === 'REJECTED'}
onClick={onOptionChange}
/>
<ApplicantFilterButton
value={'PENDING'}
label={'심사중'}
label={`심사중 (${counts.PENDING})`}
selected={option === 'PENDING'}
onClick={onOptionChange}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import styled from '@emotion/styled';
import type { DashboardCard } from '@/pages/admin/Dashboard/types/dashboard';

type Props = Omit<DashboardCard, 'id'>;
type Props = Omit<DashboardCard, 'id'> & {
isEmpty?: boolean;
};

export const SummaryCard = ({ label, value, image }: Props) => {
export const SummaryCard = ({ label, value, image, isEmpty = false }: Props) => {
return (
<Wrapper>
<IconWrapper>{image}</IconWrapper>
<IconWrapper isEmpty={isEmpty}>{image}</IconWrapper>
<TextWrapper>
<Label>{label}</Label>
<Value>{value}</Value>
<Value isEmpty={isEmpty}>{value}</Value>
{isEmpty && <EmptyText>예정된 모집이 없습니다</EmptyText>}
</TextWrapper>
</Wrapper>
);
Expand All @@ -26,8 +29,9 @@ const Wrapper = styled.div(({ theme }) => ({
borderRadius: theme.radius.lg,
}));

const IconWrapper = styled.div(({ theme }) => ({
color: theme.colors.gray900,
const IconWrapper = styled.div<{ isEmpty?: boolean }>(({ theme, isEmpty }) => ({
color: isEmpty ? theme.colors.gray400 : theme.colors.gray900,
transition: 'color 0.2s',
}));

const TextWrapper = styled.div({
Expand All @@ -41,8 +45,15 @@ const Label = styled.p(({ theme }) => ({
color: theme.colors.gray900,
}));

const Value = styled.p(({ theme }) => ({
fontSize: '2.2rem',
const Value = styled.p<{ isEmpty?: boolean }>(({ theme, isEmpty }) => ({
fontSize: '2rem',
fontWeight: theme.font.weight.bold,
color: theme.colors.gray900,
color: isEmpty ? theme.colors.gray400 : theme.colors.gray900,
transition: 'color 0.2s',
}));

const EmptyText = styled.span(({ theme }) => ({
fontSize: '1.1rem',
color: theme.colors.gray500,
marginTop: '-0.3rem',
}));
Loading