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
55 changes: 28 additions & 27 deletions src/pages/admin/ApplicationDetail/api/comments.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Comment } from '@/pages/admin/ApplicationDetail/types/comments';

export const fetchComments = async (applicationId: number): Promise<Comment[]> => {
const response = await fetch(
import.meta.env.VITE_API_BASE_URL + `/applications/${applicationId}/comments`,
);
const url = `/api/applications/${applicationId}/comments`;
const response = await fetch(url);

if (!response.ok) throw new Error('Failed to fetch comments');
return await response.json();
};

Expand All @@ -12,26 +13,26 @@ export const createComment = async (
content: string,
rating: number,
): Promise<Comment> => {
const response = await fetch(
import.meta.env.VITE_API_BASE_URL + `/applications/${applicationId}/comments`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content, rating }),
const url = `/api/applications/${applicationId}/comments`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
);
body: JSON.stringify({ content, rating }),
});

if (!response.ok) throw new Error('Failed to create comment');
return await response.json();
};

export const deleteComment = async (applicationId: number, commentId: number): Promise<void> => {
await fetch(
import.meta.env.VITE_API_BASE_URL + `/applications/${applicationId}/comments/${commentId}`,
{
method: 'DELETE',
},
);
const url = `/api/applications/${applicationId}/comments/${commentId}`;
const response = await fetch(url, {
method: 'DELETE',
});

if (!response.ok) throw new Error('Failed to delete comment');
};

export const updateComment = async (
Expand All @@ -40,15 +41,15 @@ export const updateComment = async (
content: string,
rating: number,
): Promise<Comment> => {
const response = await fetch(
import.meta.env.VITE_API_BASE_URL + `/applications/${applicationId}/comments/${commentId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content, rating }),
const url = `/api/applications/${applicationId}/comments/${commentId}`;
const response = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
);
body: JSON.stringify({ content, rating }),
});

if (!response.ok) throw new Error('Failed to update comment');
return await response.json();
};
15 changes: 10 additions & 5 deletions src/pages/admin/ApplicationDetail/api/detailApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@ export const fetchDetailApplication = async (
clubId: number,
applicantId: number,
): Promise<DetailApplication> => {
const response = await fetch(
import.meta.env.VITE_API_BASE_URL + `/clubs/${clubId}/applicants/${applicantId}/application`,
);
const url = `/api/clubs/${clubId}/applicants/${applicantId}/application`;
const response = await fetch(url);

if (!response.ok) throw new Error('지원서 상세 정보를 가져오지 못했습니다');
return await response.json();
};

export const updateApplicationStatus = async (
applicationId: number,
status: DetailApplication['status'],
): Promise<void> => {
await fetch(import.meta.env.VITE_API_BASE_URL + `/applications/${applicationId}`, {
): Promise<unknown> => {
const url = `/api/applications/${applicationId}`;
const response = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status }),
});

if (!response.ok) throw new Error('지원서 상태를 업데이트하지 못했습니다');
return await response.json();
};
18 changes: 9 additions & 9 deletions src/pages/admin/ClubDetailEdit/api/clubDetailEdit.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import type { ClubDetailEdit } from '../types/clubDetailEdit';

const BASE_URL = import.meta.env.VITE_API_BASE_URL;

export const fetchClubDetailEdit = async (clubId: string | number): Promise<ClubDetailEdit> => {
const res = await fetch(`${BASE_URL}/clubs/${clubId}`);
if (!res.ok) {
const url = `/api/clubs/${clubId}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error('동아리 상세 수정 데이터를 가져오는데 실패했습니다.');
}
return res.json() as Promise<ClubDetailEdit>;
return response.json() as Promise<ClubDetailEdit>;
};

export const updateClubDetailEdit = async (
clubId: string | number,
updatedData: Partial<ClubDetailEdit>,
): Promise<ClubDetailEdit> => {
const res = await fetch(`${BASE_URL}/clubs/${clubId}`, {
const url = `/api/clubs/${clubId}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedData),
});

if (!res.ok) {
if (!response.ok) {
throw new Error('동아리 상세 정보를 수정하는데 실패했습니다.');
}

return res.json() as Promise<ClubDetailEdit>;
return response.json() as Promise<ClubDetailEdit>;
};
23 changes: 8 additions & 15 deletions src/pages/admin/Dashboard/api/applicant.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import type {
ApplicantData,
ApplicationFilterOption,
} from '@/pages/admin/Dashboard/types/dashboard';
import type { ApplicantData } from '@/pages/admin/Dashboard/types/dashboard';

export const fetchApplicants = async (
clubId: number,
status?: ApplicationFilterOption,
): Promise<ApplicantData[]> => {
const url = new URL(
import.meta.env.VITE_API_BASE_URL + `/clubs/${clubId}/applicants`,
window.location.origin,
);
if (status && status !== 'ALL') {
url.searchParams.set('status', status);
export const fetchApplicants = async (clubId: number): Promise<ApplicantData[]> => {
const url = `/api/clubs/${clubId}/dashboard/applicants`;
const response = await fetch(url);

if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}

const response = await fetch(url.toString());
return await response.json();
};
2 changes: 1 addition & 1 deletion src/pages/admin/Dashboard/hooks/useApplicants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const useApplicants = (
): UseApiQueryResult<ApplicantData[]> => {
const { data, isLoading, error } = useQuery({
queryKey: ['applicants', clubId, status],
queryFn: () => fetchApplicants(clubId, status),
queryFn: () => fetchApplicants(clubId),
staleTime: 1000 * 60 * 2,
});
Comment on lines 13 to 17
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix cache key to match API call signature.

The queryKey at Line 14 includes status, but the API call at Line 15 no longer passes status to fetchApplicants. This creates a cache key mismatch:

  • Different status values create separate cache entries
  • But all entries fetch the same unfiltered data
  • This wastes memory and creates confusing cache behavior

Apply this diff to align the cache key with the API call:

   const { data, isLoading, error } = useQuery({
-    queryKey: ['applicants', clubId, status],
+    queryKey: ['applicants', clubId],
     queryFn: () => fetchApplicants(clubId),
     staleTime: 1000 * 60 * 2,
   });

If client-side filtering by status is still needed, implement it in the hook's return value instead:

return {
  data: status && status !== '전체' 
    ? (data || []).filter(applicant => applicant.status === status)
    : (data || []),
  isLoading,
  error,
};
🤖 Prompt for AI Agents
In src/pages/admin/Dashboard/hooks/useApplicants.ts around lines 13 to 17, the
queryKey currently includes `status` while the queryFn calls
`fetchApplicants(clubId)` without `status`, causing cache key mismatch; remove
`status` from the `queryKey` so it matches the actual API call, and if you still
need client-side filtering, apply the provided filtering logic in the hook's
return value to filter `data` by `status` (treating '전체' as no filter) while
returning `isLoading` and `error` unchanged.


Expand Down
23 changes: 13 additions & 10 deletions src/pages/user/Apply/api/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import type {
} from '@/pages/user/Apply/type/apply.ts';

export const fetchApplicationForm = async (Id: number): Promise<ApplicationForm> => {
const response = await fetch(import.meta.env.VITE_API_BASE_URL + `/clubs/${Id}/apply`);
const url = `/api/clubs/${Id}/apply`;
const response = await fetch(url);

if (!response.ok) throw new Error('지원서 양식을 가져오지 못했습니다');
return await response.json();
};

Expand All @@ -17,16 +20,16 @@ export const postApplicationForm = async (
): Promise<ApplicationFormRequest> => {
const applicationDto = applicationFormDto(formData, questionArray);

const response = await fetch(
import.meta.env.VITE_API_BASE_URL + `/clubs/${clubId}/apply-submit`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(applicationDto),
const url = `/api/clubs/${clubId}/apply-submit`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
);
body: JSON.stringify(applicationDto),
});

if (!response.ok) throw new Error('지원서 양식을 제출하지 못했습니다');
return await response.json();
};

Expand Down
12 changes: 7 additions & 5 deletions src/pages/user/ClubDetail/api/clubDetail.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { ClubDetail } from '../types/clubDetail';

const BASE_URL = import.meta.env.VITE_API_BASE_URL;

export const fetchClubDetail = async (clubId: string | number): Promise<ClubDetail> => {
const res = await fetch(`${BASE_URL}/clubs/${clubId}`);
if (!res.ok) throw new Error('동아리 상세 정보를 가져오는데 실패했습니다.');
return res.json() as Promise<ClubDetail>;
const url = `/api/clubs/${clubId}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error('동아리 상세 정보를 가져오는데 실패했습니다.');
}
return response.json() as Promise<ClubDetail>;
};
6 changes: 3 additions & 3 deletions src/pages/user/Main/api/club.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export type ClubResponse = {
};

export async function getClubsByCategory(filter: ClubCategoryEng): Promise<ClubResponse> {
const response = await fetch(
import.meta.env.VITE_API_BASE_URL + `/clubs/search/category?category=${filter}`,
);
const url = `/api/clubs?category=${filter}`;
const response = await fetch(url);
Comment on lines +9 to +10
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

Critical: Relative /api requests will 404 in production

By dropping import.meta.env.VITE_API_BASE_URL and hardcoding /api, every fetch now assumes a same-origin reverse proxy. In production builds there is no Vite dev proxy, so deployments that serve the SPA from S3/CloudFront (or any origin different from the backend) will start issuing /api/... requests to the static host and immediately fail with 404/CORS. We just lost the ability to point the frontend at the real backend domain.

Please keep using the environment-provided base URL (and only fall back to /api when it is intentionally empty) and apply the same fix to every API module touched in this PR. Example patch:

-  const url = `/api/clubs?category=${filter}`;
-  const response = await fetch(url);
+  const endpoint = `/clubs?category=${filter}`;
+  const baseUrl = import.meta.env.VITE_API_BASE_URL ?? '';
+  const response = await fetch(`${baseUrl}${endpoint}`);

This preserves production correctness while still letting you set VITE_API_BASE_URL="" for dev to leverage the proxy. Please reflect the same pattern across the other updated API files.

📝 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 url = `/api/clubs?category=${filter}`;
const response = await fetch(url);
const endpoint = `/clubs?category=${filter}`;
const baseUrl = import.meta.env.VITE_API_BASE_URL ?? '';
const response = await fetch(`${baseUrl}${endpoint}`);
🤖 Prompt for AI Agents
In src/pages/user/Main/api/club.ts around lines 9-10, the code hardcodes a
relative "/api" base which will 404 in production when the frontend is served
from a different origin; restore use of import.meta.env.VITE_API_BASE_URL with a
fallback to "/api" (e.g. const base = import.meta.env.VITE_API_BASE_URL ||
'/api') and build the fetch URL by joining base and the path safely (avoid
duplicate slashes), then replace the hardcoded const url assignment with this
composed URL; apply the same pattern to every other API module changed in this
PR.


return await response.json();
}
38 changes: 25 additions & 13 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import { defineConfig, loadEnv, type ConfigEnv } from 'vite';

// https://vite.dev/config/
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['@emotion/babel-plugin'],
export default ({ mode }: ConfigEnv) => {
const env = loadEnv(mode, process.cwd(), '');
return defineConfig({
plugins: [
react({
babel: {
plugins: ['@emotion/babel-plugin'],
},
}),
],
resolve: {
alias: {
'@': '/src',
},
}),
],
resolve: {
alias: {
'@': '/src',
},
},
});
server: {
proxy: {
'/api': {
target: env.VITE_API_BASE_URL,
changeOrigin: true,
secure: false,
},
},
},
});
};