Merge remote-tracking branch 'origin/main' into iipbs/sn/dev

This commit is contained in:
yschoi
2025-07-17 17:55:06 +09:00
23 changed files with 518 additions and 329 deletions
@@ -185,6 +185,15 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
...prev, ...prev,
[siteId]: value, [siteId]: value,
})); }));
// 상시 선택 시 게시 기간 미설정
if (value === "99") {
handleUpdateFields(siteId)({
pstgPrdSeCd: value,
});
return;
}
setRememberPeriodDate((prev) => ({ setRememberPeriodDate((prev) => ({
...prev, ...prev,
[siteId]: { from, to }, [siteId]: { from, to },
@@ -20,7 +20,7 @@ import {
Skeleton, Skeleton,
} from "@common/components"; } from "@common/components";
import { AttachmentApi } from "@/entities/attachment"; import { AttachmentApi } from "@/entities/attachment";
import { PostApi, type PostParams } from "@/entities/post"; import { PostApi, PostPageGuard, type PostParams } from "@/entities/post";
import { useAlertStore, useMenuStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useMenuStore, useUserStore } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -103,143 +103,147 @@ function FaqPage({ bbsId }: { bbsId: string }) {
}; };
return ( return (
<div className="card card-border !mt-6"> <PostPageGuard bbsId={bbsId}>
<div className="card card-border"> <div className="card card-border !mt-6">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2"> <div className="card card-border">
<Label className="mr-2 text-label">검색</Label> <form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Select <Label className="mr-2 text-label">검색</Label>
value={postParams.keywordType} <Select
onValueChange={(keywordType) => { value={postParams.keywordType}
setPostParams({ ...postParams, keywordType }); onValueChange={(keywordType) => {
}} setPostParams({ ...postParams, keywordType });
> }}
<SelectTrigger className="h-[3.2rem] w-[10rem]"> >
<SelectValue placeholder="선택" /> <SelectTrigger className="h-[3.2rem] w-[10rem]">
</SelectTrigger> <SelectValue placeholder="선택" />
<SelectContent> </SelectTrigger>
<SelectItem value="title">제목</SelectItem> <SelectContent>
<SelectItem value="all">제목+내용</SelectItem> <SelectItem value="title">제목</SelectItem>
<SelectItem value="writer">작성자</SelectItem> <SelectItem value="all">제목+내용</SelectItem>
</SelectContent> <SelectItem value="writer">작성자</SelectItem>
</Select> </SelectContent>
<Input </Select>
value={postParams.keyword} <Input
onChange={(e) => value={postParams.keyword}
setPostParams({ ...postParams, keyword: e.target.value }) onChange={(e) =>
} setPostParams({ ...postParams, keyword: e.target.value })
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]" }
/> className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
<Button className="h-[3.2rem]" type="submit"> />
조회 <Button className="h-[3.2rem]" type="submit">
</Button> 조회
</form> </Button>
</div> </form>
<div className="card card-border"> </div>
<div className="card-header"> <div className="card card-border">
<div className="flex w-full items-center justify-between"> <div className="card-header">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3> <div className="flex w-full items-center justify-between">
<div className="flex gap-2"> <h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
{pageMenuAuthority?.regAuthrtYn === "Y" && ( <div className="flex gap-2">
<Button onClick={() => router.push(`${pathname}/register`)}> {pageMenuAuthority?.regAuthrtYn === "Y" && (
등록 <Button onClick={() => router.push(`${pathname}/register`)}>
</Button> 등록
)} </Button>
{pageMenuAuthority?.delAuthrtYn === "Y" && ( )}
<Button {pageMenuAuthority?.delAuthrtYn === "Y" && (
color="red" <Button
onClick={onSelectedPostDelete} color="red"
disabled={!selectedPostNumbers.length} onClick={onSelectedPostDelete}
> disabled={!selectedPostNumbers.length}
선택삭제 >
</Button> 선택삭제
)} </Button>
)}
</div>
</div> </div>
</div> </div>
</div> <div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="card !m-[1.2rem] h-full overflow-auto !p-0"> <div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4"> <Checkbox
<Checkbox id="all-select"
id="all-select" checked={
checked={selectedPostNumbers.length === postList?.content.length} selectedPostNumbers.length === postList?.content.length
onClick={(e) => e.stopPropagation()} }
onCheckedChange={handleAllCheckboxChange} onClick={(e) => e.stopPropagation()}
/> onCheckedChange={handleAllCheckboxChange}
<Label htmlFor="all-select" className="text-2xl"> />
전체선택 <Label htmlFor="all-select" className="text-2xl">
</Label> 전체선택
</Label>
</div>
{isFetched ? (
<Accordion
type="multiple"
className="secondary flex flex-col gap-4"
>
{postList?.content.map((item) => (
<div key={item.pstNo} className="flex items-start gap-2">
<Checkbox
id={item.pstNo.toString()}
checked={selectedPostNumbers.includes(item.pstNo)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) =>
handleCheckboxChange(item.pstNo, checked)
}
className="mx-2 my-6"
/>
<AccordionItem
value={item.pstNo.toString()}
className="w-full"
>
<AccordionTrigger>
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-primary">Q</span>
<span>{item.pstTtl}</span>
</div>
<span className="px-4 text-[1.2rem] font-normal text-label">
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="flex items-start gap-4">
<span className="text-[1.4rem] font-bold text-label">
A
</span>
<div className="flex-1">
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
{item.pstCn}
</pre>
<Attachment atflCd={item.atflCd} />
</div>
<div className="flex justify-end gap-2">
<Button
size="icon"
color="white"
onClick={() =>
router.push(`${pathname}/${item.pstNo}`)
}
>
<i className="diveicon di-pencil" />
</Button>
<Button
size="icon"
color="white"
onClick={() => handleDeletePost(item.pstNo)}
>
<i className="diveicon di-trash" />
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
) : (
<Skeleton className="h-[36rem] w-full" />
)}
</div> </div>
{isFetched ? (
<Accordion
type="multiple"
className="secondary flex flex-col gap-4"
>
{postList?.content.map((item) => (
<div key={item.pstNo} className="flex items-start gap-2">
<Checkbox
id={item.pstNo.toString()}
checked={selectedPostNumbers.includes(item.pstNo)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) =>
handleCheckboxChange(item.pstNo, checked)
}
className="mx-2 my-6"
/>
<AccordionItem
value={item.pstNo.toString()}
className="w-full"
>
<AccordionTrigger>
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-primary">Q</span>
<span>{item.pstTtl}</span>
</div>
<span className="px-4 text-[1.2rem] font-normal text-label">
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="flex items-start gap-4">
<span className="text-[1.4rem] font-bold text-label">
A
</span>
<div className="flex-1">
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
{item.pstCn}
</pre>
<Attachment atflCd={item.atflCd} />
</div>
<div className="flex justify-end gap-2">
<Button
size="icon"
color="white"
onClick={() =>
router.push(`${pathname}/${item.pstNo}`)
}
>
<i className="diveicon di-pencil" />
</Button>
<Button
size="icon"
color="white"
onClick={() => handleDeletePost(item.pstNo)}
>
<i className="diveicon di-trash" />
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
) : (
<Skeleton className="h-[36rem] w-full" />
)}
</div> </div>
</div> </div>
</div> </PostPageGuard>
); );
} }
+27 -8
View File
@@ -1,5 +1,12 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; import {
CLAIM_NAME,
AUTH_USER_ID,
PORT,
PROXY_HOST,
SERVER_API_URL,
SITE_ID,
} from "./src/shared/config";
const serverApiUrl = const serverApiUrl =
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000"; process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
@@ -7,17 +14,23 @@ const serverApiUrl =
export function middleware(req: NextRequest) { export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname; const pathname = req.nextUrl.pathname;
const searchParams = req.nextUrl.searchParams; const searchParams = req.nextUrl.searchParams;
const logMessages: string[] = [
`[Middleware Request] ${new Date().toISOString()}`,
`[Environment]\n Mode: ${process.env.NODE_ENV}\n Port: ${PORT}\n Proxy Host: ${PROXY_HOST}\n Server API URL: ${SERVER_API_URL}\n Site ID: ${SITE_ID}`,
`[Request]\n Original Pathname: ${pathname}`,
];
if (pathname.startsWith("/server")) { if (pathname.startsWith("/server")) {
const queryParams = searchParams.toString();
const decodedParams = Array.from(searchParams.entries())
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
.join("&");
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
decodedParams ? `?${decodedParams}` : "" queryParams ? `?${queryParams}` : ""
}`; }`;
logMessages.push(` Rewritten URL: ${newUrl}`);
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
return NextResponse.rewrite(new URL(newUrl)); return NextResponse.rewrite(new URL(newUrl));
} }
@@ -30,6 +43,10 @@ export function middleware(req: NextRequest) {
if (token) requestHeaders.set(CLAIM_NAME, token); if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId); if (userId) requestHeaders.set(AUTH_USER_ID, userId);
logMessages.push(` Rewritten URL: ${newUrl}`);
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
return NextResponse.rewrite(new URL(newUrl), { return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders, headers: requestHeaders,
}); });
@@ -39,6 +56,8 @@ export function middleware(req: NextRequest) {
return NextResponse.redirect(new URL("/dashboard", req.url)); return NextResponse.redirect(new URL("/dashboard", req.url));
} }
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
return NextResponse.next(); return NextResponse.next();
} }
@@ -67,11 +67,22 @@ export const contentsDetailSchema = z.object({
useYn: z.string(), useYn: z.string(),
}); });
export const contentsFormSchema = contentsDetailSchema.omit({ export const contentsFormSchema = contentsDetailSchema
contsVer: true, .omit({
aprvDt: true, contsVer: true,
autzr: true, aprvDt: true,
}); autzr: true,
})
.extend({
contsSn: z.number().optional(),
aprvCd: z
.enum([
CONTENTS_APPROVE_STATUS.DRAFT,
CONTENTS_APPROVE_STATUS.REQUESTED,
CONTENTS_APPROVE_STATUS.APPROVED,
])
.optional(),
});
export type Contents = z.infer<typeof contentsSchema>; export type Contents = z.infer<typeof contentsSchema>;
export type ContentsDetail = z.infer<typeof contentsDetailSchema>; export type ContentsDetail = z.infer<typeof contentsDetailSchema>;
@@ -194,6 +194,15 @@ function PostingDialog({
...prev, ...prev,
[siteId]: value, [siteId]: value,
})); }));
// 상시 선택 시 게시 기간 미설정
if (value === "99") {
handleUpdateFields(siteId)({
pstgPrdSeCd: value,
});
return;
}
setRememberPeriodDate((prev) => ({ setRememberPeriodDate((prev) => ({
...prev, ...prev,
[siteId]: { from, to }, [siteId]: { from, to },
+1
View File
@@ -4,6 +4,7 @@ export {
userSaveSchema, userSaveSchema,
userPasswordUpdateSchema, userPasswordUpdateSchema,
userUpdateSchema, userUpdateSchema,
type UserStateCode,
type UserSearchParams, type UserSearchParams,
type UserListResponse, type UserListResponse,
type UserSaveData, type UserSaveData,
@@ -9,7 +9,8 @@ export const USER_STATE_CODE = {
삭제: "09", 삭제: "09",
} as const; } as const;
type UserStateCode = (typeof USER_STATE_CODE)[keyof typeof USER_STATE_CODE]; export type UserStateCode =
(typeof USER_STATE_CODE)[keyof typeof USER_STATE_CODE];
// 사용자 검색 파라미터 // 사용자 검색 파라미터
export interface UserSearchParams extends PageableParams { export interface UserSearchParams extends PageableParams {
+137 -132
View File
@@ -23,6 +23,7 @@ import {
import { AttachmentApi } from "@/entities/attachment"; import { AttachmentApi } from "@/entities/attachment";
import { import {
PostApi, PostApi,
PostPageGuard,
deleteReasonMessages, deleteReasonMessages,
type PostParams, type PostParams,
} from "@/entities/post"; } from "@/entities/post";
@@ -120,143 +121,147 @@ function FaqPage({ bbsId }: { bbsId: string }) {
}; };
return ( return (
<div className="card card-border !mt-6"> <PostPageGuard bbsId={bbsId}>
<div className="card card-border"> <div className="card card-border !mt-6">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2"> <div className="card card-border">
<Label className="mr-2 text-label">검색</Label> <form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Select <Label className="mr-2 text-label">검색</Label>
value={postParams.keywordType} <Select
onValueChange={(keywordType) => { value={postParams.keywordType}
setPostParams({ ...postParams, keywordType }); onValueChange={(keywordType) => {
}} setPostParams({ ...postParams, keywordType });
> }}
<SelectTrigger className="h-[3.2rem] w-[10rem]"> >
<SelectValue placeholder="선택" /> <SelectTrigger className="h-[3.2rem] w-[10rem]">
</SelectTrigger> <SelectValue placeholder="선택" />
<SelectContent> </SelectTrigger>
<SelectItem value="title">제목</SelectItem> <SelectContent>
<SelectItem value="all">제목+내용</SelectItem> <SelectItem value="title">제목</SelectItem>
<SelectItem value="writer">작성자</SelectItem> <SelectItem value="all">제목+내용</SelectItem>
</SelectContent> <SelectItem value="writer">작성자</SelectItem>
</Select> </SelectContent>
<Input </Select>
value={postParams.keyword} <Input
onChange={(e) => value={postParams.keyword}
setPostParams({ ...postParams, keyword: e.target.value }) onChange={(e) =>
} setPostParams({ ...postParams, keyword: e.target.value })
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]" }
/> className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
<Button className="h-[3.2rem]" type="submit"> />
조회 <Button className="h-[3.2rem]" type="submit">
</Button> 조회
</form> </Button>
</div> </form>
<div className="card card-border"> </div>
<div className="card-header"> <div className="card card-border">
<div className="flex w-full items-center justify-between"> <div className="card-header">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3> <div className="flex w-full items-center justify-between">
<div className="flex gap-2"> <h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<AuthorityButton <div className="flex gap-2">
authority="regAuthrtYn" <AuthorityButton
onClick={() => router.push(`${pathname}/register`)} authority="regAuthrtYn"
> onClick={() => router.push(`${pathname}/register`)}
등록 >
</AuthorityButton> 등록
<AuthorityButton </AuthorityButton>
authority="delAuthrtYn" <AuthorityButton
color="red" authority="delAuthrtYn"
onClick={onSelectedPostDelete} color="red"
disabled={!selectedPostNumbers.length} onClick={onSelectedPostDelete}
> disabled={!selectedPostNumbers.length}
선택삭제 >
</AuthorityButton> 선택삭제
</AuthorityButton>
</div>
</div> </div>
</div> </div>
</div> <div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="card !m-[1.2rem] h-full overflow-auto !p-0"> <div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4"> <Checkbox
<Checkbox id="all-select"
id="all-select" checked={
checked={selectedPostNumbers.length === postList?.content.length} selectedPostNumbers.length === postList?.content.length
onClick={(e) => e.stopPropagation()} }
onCheckedChange={handleAllCheckboxChange} onClick={(e) => e.stopPropagation()}
/> onCheckedChange={handleAllCheckboxChange}
<Label htmlFor="all-select" className="text-2xl"> />
전체선택 <Label htmlFor="all-select" className="text-2xl">
</Label> 전체선택
</Label>
</div>
{isFetched ? (
<Accordion
type="multiple"
className="secondary flex flex-col gap-4"
>
{postList?.content.map((item) => (
<div key={item.pstNo} className="flex items-start gap-2">
<Checkbox
id={item.pstNo.toString()}
checked={selectedPostNumbers.includes(item.pstNo)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) =>
handleCheckboxChange(item.pstNo, checked)
}
className="mx-2 my-6"
/>
<AccordionItem
value={item.pstNo.toString()}
className="w-full"
>
<AccordionTrigger>
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-primary">Q</span>
<span>{item.pstTtl}</span>
</div>
<span className="px-4 text-[1.2rem] font-normal text-label">
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="flex items-start gap-4">
<span className="text-[1.4rem] font-bold text-label">
A
</span>
<div className="flex-1">
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
{item.pstCn}
</pre>
<Attachment atflCd={item.atflCd} />
</div>
<div className="flex justify-end gap-2">
<Button
size="icon"
color="white"
onClick={() =>
router.push(`${pathname}/${item.pstNo}`)
}
>
<i className="diveicon di-pencil" />
</Button>
<Button
size="icon"
color="white"
onClick={() => handleDeletePost(item.pstNo)}
>
<i className="diveicon di-trash" />
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
) : (
<Skeleton className="h-[36rem] w-full" />
)}
</div> </div>
{isFetched ? (
<Accordion
type="multiple"
className="secondary flex flex-col gap-4"
>
{postList?.content.map((item) => (
<div key={item.pstNo} className="flex items-start gap-2">
<Checkbox
id={item.pstNo.toString()}
checked={selectedPostNumbers.includes(item.pstNo)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) =>
handleCheckboxChange(item.pstNo, checked)
}
className="mx-2 my-6"
/>
<AccordionItem
value={item.pstNo.toString()}
className="w-full"
>
<AccordionTrigger>
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-primary">Q</span>
<span>{item.pstTtl}</span>
</div>
<span className="px-4 text-[1.2rem] font-normal text-label">
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="flex items-start gap-4">
<span className="text-[1.4rem] font-bold text-label">
A
</span>
<div className="flex-1">
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
{item.pstCn}
</pre>
<Attachment atflCd={item.atflCd} />
</div>
<div className="flex justify-end gap-2">
<Button
size="icon"
color="white"
onClick={() =>
router.push(`${pathname}/${item.pstNo}`)
}
>
<i className="diveicon di-pencil" />
</Button>
<Button
size="icon"
color="white"
onClick={() => handleDeletePost(item.pstNo)}
>
<i className="diveicon di-trash" />
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
) : (
<Skeleton className="h-[36rem] w-full" />
)}
</div> </div>
</div> </div>
</div> </PostPageGuard>
); );
} }
+1 -1
View File
@@ -38,7 +38,7 @@ function AdminPage() {
const alert = useAlertStore((state) => state.setMessage); const alert = useAlertStore((state) => state.setMessage);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const [keywordType, setKeywordType] = useState(""); const [keywordType, setKeywordType] = useState("userId");
const [keyword, setKeyword] = useState(""); const [keyword, setKeyword] = useState("");
const [grpId, setGrpId] = useState(""); const [grpId, setGrpId] = useState("");
const [searchParams, setSearchParams] = useState<UserSearchParams>({ const [searchParams, setSearchParams] = useState<UserSearchParams>({
@@ -35,9 +35,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm", accessorKey: "pstgPrdSeNm",
header: "게시기간", header: "게시기간",
cell: ({ row }) => { cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm; const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt; const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt; const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`; return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
}, },
}, },
@@ -34,7 +34,7 @@ function CreatePage() {
<div className="card-header"> <div className="card-header">
<h3 className="text-2xl font-medium text-label">콘텐츠 등록</h3> <h3 className="text-2xl font-medium text-label">콘텐츠 등록</h3>
</div> </div>
<ContentsForm form={form} handleSave={createContents} /> <ContentsForm isEditable form={form} handleSave={createContents} />
</div> </div>
); );
} }
+13 -7
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
@@ -24,6 +25,7 @@ import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { ContentsForm } from "./contents-form"; import { ContentsForm } from "./contents-form";
function ModifyPage({ sequential }: { sequential: string }) { function ModifyPage({ sequential }: { sequential: string }) {
const router = useRouter();
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage); const alert = useAlertStore((state) => state.setMessage);
@@ -51,9 +53,11 @@ function ModifyPage({ sequential }: { sequential: string }) {
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data), mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
onSuccess: () => { onSuccess: () => {
setContsVer(0); setContsVer(0);
alert("수정되었습니다."); alert("수정되었습니다.", () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn], queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
});
router.back();
}); });
}, },
}); });
@@ -62,7 +66,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
mutationFn: (data: ContentsFormData) => mutationFn: (data: ContentsFormData) =>
ContentsApi.updateContents(contsSn, contsVer, data), ContentsApi.updateContents(contsSn, contsVer, data),
onSuccess: () => { onSuccess: () => {
alert("수정되었습니다."); alert("수정되었습니다.", router.back);
}, },
}); });
@@ -70,9 +74,11 @@ function ModifyPage({ sequential }: { sequential: string }) {
mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer), mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer),
onSuccess: () => { onSuccess: () => {
setContsVer(0); setContsVer(0);
alert("삭제되었습니다."); alert("삭제되었습니다.", () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn], queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
});
router.back();
}); });
}, },
}); });
+1 -2
View File
@@ -39,8 +39,7 @@ function FilePage() {
mutationFn: (atflId: string) => mutationFn: (atflId: string) =>
AttachmentApi.deleteAttachmentByAtflId(atflId), AttachmentApi.deleteAttachmentByAtflId(atflId),
onSuccess: () => { onSuccess: () => {
query.refetch(); alert("삭제되었습니다.", query.refetch);
alert("삭제되었습니다.");
}, },
}); });
+56 -20
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import { import {
keepPreviousData, keepPreviousData,
useQuery, useQuery,
@@ -22,6 +22,7 @@ import {
DialogFooter, DialogFooter,
Input, Input,
Checkbox, Checkbox,
Label,
} from "@common/components"; } from "@common/components";
import { import {
UserApi, UserApi,
@@ -41,7 +42,8 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
const alert = useAlertStore((state) => state.setMessage); const alert = useAlertStore((state) => state.setMessage);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({ const [userName, setUserName] = useState("");
const [searchParam, setSearchParam] = useState<UserSearchParams>({
userName: "", userName: "",
}); });
const [pagination, setPagination] = useState<PaginationState>({ const [pagination, setPagination] = useState<PaginationState>({
@@ -53,10 +55,10 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
); );
const query = useQuery({ const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userSearchQuery, pagination], queryKey: [ENDPOINT.USER_SERVICE.USERS, searchParam, pagination],
queryFn: () => queryFn: () =>
UserApi.findPageUser({ UserApi.findPageUser({
...userSearchQuery, ...searchParam,
page: pagination.pageIndex, page: pagination.pageIndex,
size: pagination.pageSize, size: pagination.pageSize,
}), }),
@@ -118,6 +120,9 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
onRowSelectionChange: setUserRowSelection, onRowSelectionChange: setUserRowSelection,
getRowId: (row) => row.userUnqId, getRowId: (row) => row.userUnqId,
}, },
dataTableOptions: {
viewPagination: true,
},
}); });
const handleAddUsers = async () => { const handleAddUsers = async () => {
@@ -144,10 +149,30 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
} }
}; };
const handleSearch = () => { const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
setPagination({ pageIndex: 0, pageSize: 10 }); e.preventDefault();
e.stopPropagation();
setPagination({
...pagination,
pageIndex: 0,
});
setSearchParam({ userName });
}; };
const resetSearch = () => {
setPagination({
pageIndex: 0,
pageSize: 10,
});
setUserName("");
setSearchParam({ userName: "" });
setUserRowSelection({});
};
useEffect(() => {
if (open) resetSearch();
}, [open]);
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -157,20 +182,31 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
<DialogHeader> <DialogHeader>
<DialogTitle>사용자 추가</DialogTitle> <DialogTitle>사용자 추가</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="mb-4 flex items-end gap-2"> <form onSubmit={handleSearch} className="mb-4 flex items-end gap-2">
<Input <div className="flex items-center gap-2">
value={userSearchQuery.userName} <Label htmlFor="userName" className="mr-2 text-label">
onChange={(e) => 사용자 이름
setUserSearchQuery({ </Label>
...userSearchQuery, <Input
userName: e.target.value, id="userName"
}) className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
} value={userName}
className="!text-[1.3rem]" onChange={(e) => setUserName(e.target.value)}
placeholder="사용자 이름으로 검색" placeholder="사용자 이름으로 검색"
/> />
<Button onClick={handleSearch}>검색</Button> </div>
</div> <Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={resetSearch}
>
초기화
</Button>
</form>
{tableComponent} {tableComponent}
<DialogFooter> <DialogFooter>
<Button color="white" onClick={() => setOpen(false)}> <Button color="white" onClick={() => setOpen(false)}>
@@ -35,9 +35,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm", accessorKey: "pstgPrdSeNm",
header: "게시기간", header: "게시기간",
cell: ({ row }) => { cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm; const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt; const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt; const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`; return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
}, },
}, },
@@ -72,7 +72,7 @@ function AddUserDialog({ onAddUsers, ...props }: AddUserDialogProps) {
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}> <DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader> <DialogHeader>
<DialogTitle>콘텐츠 조회</DialogTitle> <DialogTitle>사용자 조회</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="card card-border !m-0 flex w-full items-center gap-2"> <div className="card card-border !m-0 flex w-full items-center gap-2">
@@ -242,6 +242,16 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
</div> </div>
<div className="card max-h-[calc(100vh-58rem)] overflow-auto !p-0"> <div className="card max-h-[calc(100vh-58rem)] overflow-auto !p-0">
<Table> <Table>
<colgroup>
<col width="5%" />
<col width="5%" />
<col width="auto" />
<col width="10%" />
<col width="10%" />
<col width="10%" />
<col width="15%" />
<col width="15%" />
</colgroup>
<TableHeader className="sticky top-0"> <TableHeader className="sticky top-0">
<TableRow> <TableRow>
<TableHead> <TableHead>
@@ -300,6 +310,9 @@ const UserCells = memo(
jobPositionCodeData, jobPositionCodeData,
jobGradeCodeData, jobGradeCodeData,
}: UserCellsProps) => { }: UserCellsProps) => {
const [jbpsSeCd, setJbpsSeCd] = useState(user.jbpsSeCd);
const [jbgdSeCd, setJbgdSeCd] = useState(user.jbgdSeCd);
const updateMode = (index: number) => { const updateMode = (index: number) => {
const mode = form.watch(`userOrganization.${index}.mode`); const mode = form.watch(`userOrganization.${index}.mode`);
if (mode === "R") { if (mode === "R") {
@@ -322,17 +335,33 @@ const UserCells = memo(
<CodeSelect <CodeSelect
codeData={jobPositionCodeData} codeData={jobPositionCodeData}
defaultValue={user.jbpsSeCd} defaultValue={user.jbpsSeCd}
onValueChange={updateFormValue(index, "jbpsSeCd")} onValueChange={(value) => {
setJbpsSeCd(value);
updateFormValue(index, "jbpsSeCd")(value);
}}
{...form.register(`userOrganization.${index}.jbpsSeCd`)} {...form.register(`userOrganization.${index}.jbpsSeCd`)}
/> />
{!jbpsSeCd && (
<p className="pt-1 text-base text-destructive">
직위를 선택해주세요.
</p>
)}
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
<CodeSelect <CodeSelect
codeData={jobGradeCodeData} codeData={jobGradeCodeData}
defaultValue={user.jbgdSeCd} defaultValue={user.jbgdSeCd}
onValueChange={updateFormValue(index, "jbgdSeCd")} onValueChange={(value) => {
setJbgdSeCd(value);
updateFormValue(index, "jbgdSeCd")(value);
}}
{...form.register(`userOrganization.${index}.jbgdSeCd`)} {...form.register(`userOrganization.${index}.jbgdSeCd`)}
/> />
{!jbgdSeCd && (
<p className="pt-1 text-base text-destructive">
직급을 선택해주세요.
</p>
)}
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
<Input <Input
@@ -33,9 +33,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm", accessorKey: "pstgPrdSeNm",
header: "게시기간", header: "게시기간",
cell: ({ row }) => { cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm; const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt; const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt; const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`; return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
}, },
}, },
+18 -2
View File
@@ -3,8 +3,13 @@ import { useState } from "react";
import type { Role } from "@/entities/role"; import type { Role } from "@/entities/role";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import { MappingRoleDialog } from "../ui/mapping-role-dialog"; import { MappingRoleDialog } from "../ui/mapping-role-dialog";
import { Button } from "@common/components";
export const columns: ColumnDef<Role>[] = [ export const createColumns = ({
deleteRole,
}: {
deleteRole: (roleId: string) => void;
}): ColumnDef<Role>[] => [
{ {
id: "no", id: "no",
header: "NO", header: "NO",
@@ -39,7 +44,18 @@ export const columns: ColumnDef<Role>[] = [
{ {
id: "action", id: "action",
header: "관리", header: "관리",
cell: ({ row }) => <MappingRoleButton roleId={row.original.roleId} />, cell: ({ row }) => (
<div className="flex items-center justify-center gap-2">
<MappingRoleButton roleId={row.original.roleId} />
<Button
size="sm"
color="red"
onClick={() => deleteRole(row.original.roleId)}
>
삭제
</Button>
</div>
),
}, },
]; ];
+13 -3
View File
@@ -7,11 +7,12 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components"; import { Button, Input, Label } from "@common/components";
import { RoleApi, type Role, type RoleParams } from "@/entities/role"; import { RoleApi, type Role, type RoleParams } from "@/entities/role";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib"; import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
import { columns } from "../model/table-columns"; import { createColumns } from "../model/table-columns";
function RolePage() { function RolePage() {
const router = useRouter(); const router = useRouter();
const alert = useAlertStore((state) => state.setMessage);
const [keyword, setKeyword] = useState(""); const [keyword, setKeyword] = useState("");
const [searchParams, setSearchParams] = useState<RoleParams>({ const [searchParams, setSearchParams] = useState<RoleParams>({
@@ -33,6 +34,13 @@ function RolePage() {
}), }),
}); });
const { mutateAsync: deleteRole } = useMutationWrapper({
mutationFn: (roleId: string) => RoleApi.deleteRole(roleId),
onSuccess: () => {
alert("삭제되었습니다.", query.refetch);
},
});
const handleRowClick = (row: Role) => { const handleRowClick = (row: Role) => {
router.push(`/role/register/${row.roleId}`); router.push(`/role/register/${row.roleId}`);
}; };
@@ -40,7 +48,9 @@ function RolePage() {
const { tableComponent } = useDataTable({ const { tableComponent } = useDataTable({
query, query,
tableOptions: { tableOptions: {
columns, columns: createColumns({
deleteRole,
}),
state: { state: {
pagination, pagination,
}, },
@@ -36,9 +36,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm", accessorKey: "pstgPrdSeNm",
header: "게시기간", header: "게시기간",
cell: ({ row }) => { cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm; const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt; const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt; const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`; return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
}, },
}, },
@@ -1,6 +1,11 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import type { UserListResponse } from "@/entities/user"; import {
USER_STATE_CODE,
type UserListResponse,
type UserStateCode,
} from "@/entities/user";
import { Badge } from "@common/components";
export const columns: ColumnDef<UserListResponse>[] = [ export const columns: ColumnDef<UserListResponse>[] = [
{ {
@@ -42,8 +47,11 @@ export const columns: ColumnDef<UserListResponse>[] = [
header: "기관/부서", header: "기관/부서",
}, },
{ {
accessorKey: "userStateCodeName", accessorKey: "userStateCode",
header: "회원상태", header: "회원상태",
cell: ({ row }) => {
return getUserStateBadge(row.original.userStateCode);
},
}, },
{ {
accessorKey: "aprvYmd", accessorKey: "aprvYmd",
@@ -63,3 +71,18 @@ export const columns: ColumnDef<UserListResponse>[] = [
}, },
}, },
]; ];
const getUserStateBadge = (userStateCode: UserStateCode) => {
switch (userStateCode) {
case USER_STATE_CODE.대기:
return <Badge color="gray">대기</Badge>;
case USER_STATE_CODE.정상:
return <Badge color="green">정상</Badge>;
case USER_STATE_CODE.정지:
return <Badge color="yellow">정지</Badge>;
case USER_STATE_CODE.탈퇴:
return <Badge color="purple">탈퇴</Badge>;
case USER_STATE_CODE.삭제:
return <Badge color="red">삭제</Badge>;
}
};
@@ -16,20 +16,11 @@ function CreatePage() {
defaultValues: { defaultValues: {
userId: "", userId: "",
userName: "", userName: "",
email: "",
password: "", password: "",
pswdHnt: "", pswdHnt: "",
pswdAns: "",
mphn: "",
telno: "",
zip: "",
addr: "",
daddr: "",
laeSeCd: "", laeSeCd: "",
ogdpSeCd: "", ogdpSeCd: "",
expryYmd: "", expryYmd: "",
instSeCd: "",
enoCd: "",
}, },
}); });