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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { type FormEvent, useId, useState } from "react";
import { type FormEvent, useEffect, useId, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
Expand Down Expand Up @@ -69,6 +69,7 @@ export function UnivApplyInfoManageTab() {
const [editModal, setEditModal] = useState<EditModal>({ open: false });
const [editForm, setEditForm] = useState<UnivApplyInfoUpdatePayload>({});
const [editExtraInfoText, setEditExtraInfoText] = useState("");
const editingId = editModal.open ? editModal.id : undefined;

const [createModal, setCreateModal] = useState<CreateModal>({ open: false });
const [createForm, setCreateForm] = useState<Partial<UnivApplyInfoCreatePayload>>({});
Expand Down Expand Up @@ -103,6 +104,28 @@ export function UnivApplyInfoManageTab() {
enabled: hostSearchQuery.length > 0,
});

const editDetailQuery = useQuery({
queryKey: ["univ-apply-infos", "detail", editingId],
queryFn: () => adminApi.getUnivApplyInfo(editingId as number),
enabled: editingId !== undefined,
});

// biome-ignore lint/correctness/useExhaustiveDependencies: ์กฐํšŒ๋œ ์ƒ์„ธ ์ •๋ณด์˜ id๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งŒ ํผ์„ ์ฑ„์›Œ์•ผ ํ•˜๋ฉฐ, ๋ฆฌํŽ˜์น˜๋กœ ๋ฐ์ดํ„ฐ ์ฐธ์กฐ๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ์ˆ˜์ • ์ค‘์ธ ๊ฐ’์„ ๋ฎ์–ด์“ฐ๋ฉด ์•ˆ ๋จ
useEffect(() => {
if (!editDetailQuery.data) return;
const detail = editDetailQuery.data;
setEditForm({
studentCapacity: detail.studentCapacity,
semesterAvailableForDispatch: detail.semesterAvailableForDispatch,
semesterRequirement: detail.semesterRequirement,
detailsForLanguage: detail.detailsForLanguage,
gpaRequirement: detail.gpaRequirement,
gpaRequirementCriteria: detail.gpaRequirementCriteria,
detailsForAccommodation: detail.detailsForAccommodation,
});
setEditExtraInfoText(detail.extraInfo ? JSON.stringify(detail.extraInfo, null, 2) : "");
}, [editDetailQuery.data?.id]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Refresh cached details when reopening the same record

After an administrator saves a record and reopens it, React Query immediately returns the cached pre-update detail and then refetches it because the detail query was neither updated nor invalidated on mutation success. The effect runs for the cached value when the modal reopens, but the network response has the same id, so this dependency suppresses the second run and leaves the form showing stale values; saving again can silently overwrite the previous update. Update/remove the detail cache after mutation or key form initialization to each modal session while still protecting active edits from focus refetches.

Useful? React with ๐Ÿ‘ย / ๐Ÿ‘Ž.

Comment on lines +107 to +127

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

๐Ÿ—„๏ธ Data Integrity & Integration | ๐ŸŸ  Major | โšก Quick win

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
set -euo pipefail

FILE='apps/admin/src/components/features/univ-apply-infos/tabs/UnivApplyInfoManageTab.tsx'

ast-grep outline "$FILE" --items all
rg -n -C 8 'editDetailQuery|updateMutation|invalidateQueries|setEditModal' "$FILE"

Repository: solid-connection/solid-connect-web

Length of output: 8823


๐Ÿ Script executed:

#!/bin/bash
set -euo pipefail

FILE='apps/admin/src/types/admin.ts'
# Locate adminApi definition even if filename differs.
ast-grep outline apps/admin/src --items all 2>/dev/null | rg -n 'adminApi|getUnivApplyInfo|updateUnivApplyInfo' || true

# Print relevant sections from candidate files.
fd -t f 'admin.*\.(ts|tsx)$' apps/admin/src | while read -r f; do
  echo "===== $f ====="
  rg -n -C 6 'getUnivApplyInfo|updateUnivApplyInfo|univ-apply-infos|queryKey' "$f" || true
done

# Print exact relevant source ranges from the tab.
sed -n '1,230p' "$FILE" 2>/dev/null || true
sed -n '129,143p' apps/admin/src/components/features/univ-apply-infos/tabs/UnivApplyInfoManageTab.tsx

Repository: solid-connection/solid-connect-web

Length of output: 4003


์ˆ˜์ • ์„ฑ๊ณต ํ›„ ์ƒ์„ธ ์ฟผ๋ฆฌ๋„ ๋ฌดํšจํ™”ํ•˜์„ธ์š”.

์ˆ˜์ • ์„ฑ๊ณต ํ›„ ["univ-apply-infos", "search"]๋งŒ invalidateQueriesํ•ฉ๋‹ˆ๋‹ค. ๊ฐ™์€ ํ•ญ๋ชฉ์„ ๋‹ค์‹œ ์—ด๋ฉด ์ด์ „ ์ƒ์„ธ ์บ์‹œ๊ฐ€ ํผ์— ์ ์šฉ๋  ์ˆ˜ ์žˆ๊ณ , ๋™์ผํ•œ detail.id ์žฌ์กฐํšŒ๋Š” useEffect๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•˜์ง€ ์•Š์•„ ์ตœ์‹  ๊ฐ’์ด ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ˆ˜์ • ์˜ˆ์‹œ
- onSuccess: async () => {
-   await queryClient.invalidateQueries({
-     queryKey: ["univ-apply-infos", "search"],
-   });
+ onSuccess: async (_response, { id }) => {
+   await Promise.all([
+     queryClient.invalidateQueries({
+       queryKey: ["univ-apply-infos", "search"],
+     }),
+     queryClient.invalidateQueries({
+       queryKey: ["univ-apply-infos", "detail", id],
+     }),
+   ]);
    setEditModal({ open: false });
    toast.success("์ง€์› ๋Œ€ํ•™ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.");
  },

๋™์ผ ํ•ญ๋ชฉ์„ ์ˆ˜์ •ํ•œ ๋’ค ๋‹ค์‹œ ์—ด์–ด ์ตœ์‹  ๊ฐ’์ด ํ‘œ์‹œ๋˜๋Š” ๊ฒ€์ฆ๋„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

๐Ÿค– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/admin/src/components/features/univ-apply-infos/tabs/UnivApplyInfoManageTab.tsx`
around lines 107 - 127, ์ˆ˜์ • ์„ฑ๊ณต ์ฒ˜๋ฆฌ์—์„œ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋ฟ ์•„๋‹ˆ๋ผ ์ƒ์„ธ ์ฟผ๋ฆฌ๋„ ๋ฌดํšจํ™”ํ•˜๋„๋ก ์—…๋ฐ์ดํŠธํ•˜์„ธ์š”. ์ˆ˜์ • ๋Œ€์ƒ์ธ
`["univ-apply-infos", "detail", editingId]`๋ฅผ `invalidateQueries`์— ํฌํ•จํ•ด ์žฌ์กฐํšŒ ์‹œ ์ตœ์‹ 
๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , `editDetailQuery.data?.id`๊ฐ€ ๋™์ผํ•ด๋„ ์ตœ์‹  ๊ฐ’์œผ๋กœ ํผ์ด ๊ฐฑ์‹ ๋˜๋„๋ก ํ•˜์„ธ์š”. ๋™์ผ ํ•ญ๋ชฉ์„ ์ˆ˜์ •ํ•œ ๋’ค ๋‹ค์‹œ
์—ด์—ˆ์„ ๋•Œ ์ตœ์‹  ๊ฐ’์ด ํ‘œ์‹œ๋˜๋Š” ๊ฒ€์ฆ๋„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.


const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: UnivApplyInfoUpdatePayload }) =>
adminApi.updateUnivApplyInfo(id, data),
Expand Down Expand Up @@ -158,7 +181,7 @@ export function UnivApplyInfoManageTab() {
};

const handleOpenEdit = (item: UnivApplyInfoSearchResult) => {
setEditForm({ studentCapacity: item.studentCapacity });
setEditForm({});
setEditExtraInfoText("");
setEditModal({ open: true, id: item.id, name: item.koreanName });
};
Expand Down Expand Up @@ -363,85 +386,91 @@ export function UnivApplyInfoManageTab() {
โœ•
</button>
</div>
<form onSubmit={handleEditSubmit} className="space-y-3 p-5">
<div className="space-y-1">
<label htmlFor={`${uid}-edit-student-capacity`} className="typo-sb-11 text-k-700">
์ •์›
</label>
<Input
id={`${uid}-edit-student-capacity`}
type="number"
min={1}
value={editForm.studentCapacity ?? ""}
onChange={(e) =>
setEditForm((p) => ({
...p,
studentCapacity: e.target.value ? Number(e.target.value) : undefined,
}))
}
/>
</div>
<div className="space-y-1">
<label htmlFor={`${uid}-edit-semester-dispatch`} className="typo-sb-11 text-k-700">
ํŒŒ๊ฒฌ ๊ฐ€๋Šฅ ํ•™๊ธฐ
</label>
<select
id={`${uid}-edit-semester-dispatch`}
value={editForm.semesterAvailableForDispatch ?? ""}
onChange={(e) =>
setEditForm((p) => ({
...p,
semesterAvailableForDispatch: e.target.value || undefined,
}))
}
className="h-9 w-full rounded-md border border-k-200 bg-k-0 px-3 typo-regular-4 text-k-700 outline-none focus-visible:border-primary"
>
<option value="">โ€” ์„ ํƒ ์•ˆ ํ•จ โ€”</option>
{SEMESTER_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</div>
{EDIT_TEXT_FIELDS.map(({ key, label }) => (
<div key={key} className="space-y-1">
<label htmlFor={`${uid}-edit-${key}`} className="typo-sb-11 text-k-700">
{label}
{editDetailQuery.isLoading ? (
<p className="p-5 typo-regular-4 text-k-500">๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...</p>
) : editDetailQuery.isError ? (
<p className="p-5 typo-regular-4 text-magic-danger">์ƒ์„ธ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.</p>
) : (
<form onSubmit={handleEditSubmit} className="space-y-3 p-5">
<div className="space-y-1">
<label htmlFor={`${uid}-edit-student-capacity`} className="typo-sb-11 text-k-700">
์ •์›
</label>
<Input
id={`${uid}-edit-${key}`}
value={editForm[key] ?? ""}
id={`${uid}-edit-student-capacity`}
type="number"
min={1}
value={editForm.studentCapacity ?? ""}
onChange={(e) =>
setEditForm((p) => ({
...p,
[key]: e.target.value || undefined,
studentCapacity: e.target.value ? Number(e.target.value) : undefined,
}))
}
/>
</div>
))}
<div className="space-y-1">
<label htmlFor={`${uid}-edit-extra-info`} className="typo-sb-11 text-k-700">
์ถ”๊ฐ€ ์ •๋ณด (JSON)
</label>
<Textarea
id={`${uid}-edit-extra-info`}
value={editExtraInfoText}
onChange={(e) => setEditExtraInfoText(e.target.value)}
placeholder='{"key": "value"}'
className="h-20 font-mono"
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="secondary" onClick={() => setEditModal({ open: false })}>
์ทจ์†Œ
</Button>
<Button type="submit" disabled={updateMutation.isPending}>
{updateMutation.isPending ? "์ €์žฅ ์ค‘..." : "์ €์žฅ"}
</Button>
</div>
</form>
<div className="space-y-1">
<label htmlFor={`${uid}-edit-semester-dispatch`} className="typo-sb-11 text-k-700">
ํŒŒ๊ฒฌ ๊ฐ€๋Šฅ ํ•™๊ธฐ
</label>
<select
id={`${uid}-edit-semester-dispatch`}
value={editForm.semesterAvailableForDispatch ?? ""}
onChange={(e) =>
setEditForm((p) => ({
...p,
semesterAvailableForDispatch: e.target.value || undefined,
}))
}
className="h-9 w-full rounded-md border border-k-200 bg-k-0 px-3 typo-regular-4 text-k-700 outline-none focus-visible:border-primary"
>
<option value="">โ€” ์„ ํƒ ์•ˆ ํ•จ โ€”</option>
{SEMESTER_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</div>
{EDIT_TEXT_FIELDS.map(({ key, label }) => (
<div key={key} className="space-y-1">
<label htmlFor={`${uid}-edit-${key}`} className="typo-sb-11 text-k-700">
{label}
</label>
<Input
id={`${uid}-edit-${key}`}
value={editForm[key] ?? ""}
onChange={(e) =>
setEditForm((p) => ({
...p,
[key]: e.target.value || undefined,
}))
}
/>
</div>
))}
<div className="space-y-1">
<label htmlFor={`${uid}-edit-extra-info`} className="typo-sb-11 text-k-700">
์ถ”๊ฐ€ ์ •๋ณด (JSON)
</label>
<Textarea
id={`${uid}-edit-extra-info`}
value={editExtraInfoText}
onChange={(e) => setEditExtraInfoText(e.target.value)}
placeholder='{"key": "value"}'
className="h-20 font-mono"
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="secondary" onClick={() => setEditModal({ open: false })}>
์ทจ์†Œ
</Button>
<Button type="submit" disabled={updateMutation.isPending}>
{updateMutation.isPending ? "์ €์žฅ ์ค‘..." : "์ €์žฅ"}
</Button>
</div>
</form>
)}
</div>
</div>
)}
Expand Down
3 changes: 3 additions & 0 deletions apps/admin/src/lib/api/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,9 @@ export const adminApi = {
deleteHostUniversity: (id: number) =>
axiosInstance.delete<void>(`/admin/host-universities/${id}`).then((res) => res.data),

getUnivApplyInfo: (id: number) =>
axiosInstance.get<UnivApplyInfoManageResponse>(`/admin/univ-apply-infos/${id}`).then((res) => res.data),

createUnivApplyInfo: (data: UnivApplyInfoCreatePayload) =>
axiosInstance.post<UnivApplyInfoManageResponse>("/admin/univ-apply-infos", data).then((res) => res.data),

Expand Down
Loading