-
Notifications
You must be signed in to change notification settings - Fork 3
๐ ์ง์ ๋ํ ์์ ๋ชจ๋ฌ์์ ์ ์ ์ธ ํ๋๊ฐ ์ฑ์์ง์ง ์๋ ๋ฌธ์ ์์ #620
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
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 |
|---|---|---|
| @@ -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"; | ||
|
|
@@ -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>>({}); | ||
|
|
@@ -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]); | ||
|
Comment on lines
+107
to
+127
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. ๐๏ธ 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.tsxRepository: solid-connection/solid-connect-web Length of output: 4003 ์์ ์ฑ๊ณต ํ ์์ธ ์ฟผ๋ฆฌ๋ ๋ฌดํจํํ์ธ์. ์์ ์ฑ๊ณต ํ ์์ ์์- 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 |
||
|
|
||
| const updateMutation = useMutation({ | ||
| mutationFn: ({ id, data }: { id: number; data: UnivApplyInfoUpdatePayload }) => | ||
| adminApi.updateUnivApplyInfo(id, data), | ||
|
|
@@ -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 }); | ||
| }; | ||
|
|
@@ -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> | ||
| )} | ||
|
|
||
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.
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 ๐ย / ๐.