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,
[siteId]: value,
}));
// 상시 선택 시 게시 기간 미설정
if (value === "99") {
handleUpdateFields(siteId)({
pstgPrdSeCd: value,
});
return;
}
setRememberPeriodDate((prev) => ({
...prev,
[siteId]: { from, to },
@@ -20,7 +20,7 @@ import {
Skeleton,
} from "@common/components";
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 { ENDPOINT } from "@/shared/config";
import dayjs from "dayjs";
@@ -103,143 +103,147 @@ function FaqPage({ bbsId }: { bbsId: string }) {
};
return (
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
{pageMenuAuthority?.regAuthrtYn === "Y" && (
<Button onClick={() => router.push(`${pathname}/register`)}>
등록
</Button>
)}
{pageMenuAuthority?.delAuthrtYn === "Y" && (
<Button
color="red"
onClick={onSelectedPostDelete}
disabled={!selectedPostNumbers.length}
>
선택삭제
</Button>
)}
<PostPageGuard bbsId={bbsId}>
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
{pageMenuAuthority?.regAuthrtYn === "Y" && (
<Button onClick={() => router.push(`${pathname}/register`)}>
등록
</Button>
)}
{pageMenuAuthority?.delAuthrtYn === "Y" && (
<Button
color="red"
onClick={onSelectedPostDelete}
disabled={!selectedPostNumbers.length}
>
선택삭제
</Button>
)}
</div>
</div>
</div>
</div>
<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">
<Checkbox
id="all-select"
checked={selectedPostNumbers.length === postList?.content.length}
onClick={(e) => e.stopPropagation()}
onCheckedChange={handleAllCheckboxChange}
/>
<Label htmlFor="all-select" className="text-2xl">
전체선택
</Label>
<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">
<Checkbox
id="all-select"
checked={
selectedPostNumbers.length === postList?.content.length
}
onClick={(e) => e.stopPropagation()}
onCheckedChange={handleAllCheckboxChange}
/>
<Label htmlFor="all-select" className="text-2xl">
전체선택
</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>
{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>
</PostPageGuard>
);
}
+27 -8
View File
@@ -1,5 +1,12 @@
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 =
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
@@ -7,17 +14,23 @@ const serverApiUrl =
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
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")) {
const decodedParams = Array.from(searchParams.entries())
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
.join("&");
const queryParams = searchParams.toString();
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));
}
@@ -30,6 +43,10 @@ export function middleware(req: NextRequest) {
if (token) requestHeaders.set(CLAIM_NAME, token);
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), {
headers: requestHeaders,
});
@@ -39,6 +56,8 @@ export function middleware(req: NextRequest) {
return NextResponse.redirect(new URL("/dashboard", req.url));
}
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
return NextResponse.next();
}
@@ -67,11 +67,22 @@ export const contentsDetailSchema = z.object({
useYn: z.string(),
});
export const contentsFormSchema = contentsDetailSchema.omit({
contsVer: true,
aprvDt: true,
autzr: true,
});
export const contentsFormSchema = contentsDetailSchema
.omit({
contsVer: 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 ContentsDetail = z.infer<typeof contentsDetailSchema>;
@@ -194,6 +194,15 @@ function PostingDialog({
...prev,
[siteId]: value,
}));
// 상시 선택 시 게시 기간 미설정
if (value === "99") {
handleUpdateFields(siteId)({
pstgPrdSeCd: value,
});
return;
}
setRememberPeriodDate((prev) => ({
...prev,
[siteId]: { from, to },
+1
View File
@@ -4,6 +4,7 @@ export {
userSaveSchema,
userPasswordUpdateSchema,
userUpdateSchema,
type UserStateCode,
type UserSearchParams,
type UserListResponse,
type UserSaveData,
@@ -9,7 +9,8 @@ export const USER_STATE_CODE = {
삭제: "09",
} 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 {
+137 -132
View File
@@ -23,6 +23,7 @@ import {
import { AttachmentApi } from "@/entities/attachment";
import {
PostApi,
PostPageGuard,
deleteReasonMessages,
type PostParams,
} from "@/entities/post";
@@ -120,143 +121,147 @@ function FaqPage({ bbsId }: { bbsId: string }) {
};
return (
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
<AuthorityButton
authority="regAuthrtYn"
onClick={() => router.push(`${pathname}/register`)}
>
등록
</AuthorityButton>
<AuthorityButton
authority="delAuthrtYn"
color="red"
onClick={onSelectedPostDelete}
disabled={!selectedPostNumbers.length}
>
선택삭제
</AuthorityButton>
<PostPageGuard bbsId={bbsId}>
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
<AuthorityButton
authority="regAuthrtYn"
onClick={() => router.push(`${pathname}/register`)}
>
등록
</AuthorityButton>
<AuthorityButton
authority="delAuthrtYn"
color="red"
onClick={onSelectedPostDelete}
disabled={!selectedPostNumbers.length}
>
선택삭제
</AuthorityButton>
</div>
</div>
</div>
</div>
<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">
<Checkbox
id="all-select"
checked={selectedPostNumbers.length === postList?.content.length}
onClick={(e) => e.stopPropagation()}
onCheckedChange={handleAllCheckboxChange}
/>
<Label htmlFor="all-select" className="text-2xl">
전체선택
</Label>
<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">
<Checkbox
id="all-select"
checked={
selectedPostNumbers.length === postList?.content.length
}
onClick={(e) => e.stopPropagation()}
onCheckedChange={handleAllCheckboxChange}
/>
<Label htmlFor="all-select" className="text-2xl">
전체선택
</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>
{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>
</PostPageGuard>
);
}
+1 -1
View File
@@ -38,7 +38,7 @@ function AdminPage() {
const alert = useAlertStore((state) => state.setMessage);
const { confirm } = useConfirm();
const [keywordType, setKeywordType] = useState("");
const [keywordType, setKeywordType] = useState("userId");
const [keyword, setKeyword] = useState("");
const [grpId, setGrpId] = useState("");
const [searchParams, setSearchParams] = useState<UserSearchParams>({
@@ -35,9 +35,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm",
header: "게시기간",
cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
},
},
@@ -34,7 +34,7 @@ function CreatePage() {
<div className="card-header">
<h3 className="text-2xl font-medium text-label">콘텐츠 등록</h3>
</div>
<ContentsForm form={form} handleSave={createContents} />
<ContentsForm isEditable form={form} handleSave={createContents} />
</div>
);
}
+13 -7
View File
@@ -1,5 +1,6 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
@@ -24,6 +25,7 @@ import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import { ContentsForm } from "./contents-form";
function ModifyPage({ sequential }: { sequential: string }) {
const router = useRouter();
const { confirm } = useConfirm();
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
@@ -51,9 +53,11 @@ function ModifyPage({ sequential }: { sequential: string }) {
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
onSuccess: () => {
setContsVer(0);
alert("수정되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
alert("수정되었습니다.", () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
});
router.back();
});
},
});
@@ -62,7 +66,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
mutationFn: (data: ContentsFormData) =>
ContentsApi.updateContents(contsSn, contsVer, data),
onSuccess: () => {
alert("수정되었습니다.");
alert("수정되었습니다.", router.back);
},
});
@@ -70,9 +74,11 @@ function ModifyPage({ sequential }: { sequential: string }) {
mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer),
onSuccess: () => {
setContsVer(0);
alert("삭제되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
alert("삭제되었습니다.", () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
});
router.back();
});
},
});
+1 -2
View File
@@ -39,8 +39,7 @@ function FilePage() {
mutationFn: (atflId: string) =>
AttachmentApi.deleteAttachmentByAtflId(atflId),
onSuccess: () => {
query.refetch();
alert("삭제되었습니다.");
alert("삭제되었습니다.", query.refetch);
},
});
+56 -20
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import {
keepPreviousData,
useQuery,
@@ -22,6 +22,7 @@ import {
DialogFooter,
Input,
Checkbox,
Label,
} from "@common/components";
import {
UserApi,
@@ -41,7 +42,8 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
const alert = useAlertStore((state) => state.setMessage);
const [open, setOpen] = useState(false);
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
const [userName, setUserName] = useState("");
const [searchParam, setSearchParam] = useState<UserSearchParams>({
userName: "",
});
const [pagination, setPagination] = useState<PaginationState>({
@@ -53,10 +55,10 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
);
const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userSearchQuery, pagination],
queryKey: [ENDPOINT.USER_SERVICE.USERS, searchParam, pagination],
queryFn: () =>
UserApi.findPageUser({
...userSearchQuery,
...searchParam,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -118,6 +120,9 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
onRowSelectionChange: setUserRowSelection,
getRowId: (row) => row.userUnqId,
},
dataTableOptions: {
viewPagination: true,
},
});
const handleAddUsers = async () => {
@@ -144,10 +149,30 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
}
};
const handleSearch = () => {
setPagination({ pageIndex: 0, pageSize: 10 });
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
@@ -157,20 +182,31 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
<DialogHeader>
<DialogTitle>사용자 추가</DialogTitle>
</DialogHeader>
<div className="mb-4 flex items-end gap-2">
<Input
value={userSearchQuery.userName}
onChange={(e) =>
setUserSearchQuery({
...userSearchQuery,
userName: e.target.value,
})
}
className="!text-[1.3rem]"
placeholder="사용자 이름으로 검색"
/>
<Button onClick={handleSearch}>검색</Button>
</div>
<form onSubmit={handleSearch} className="mb-4 flex items-end gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="userName" className="mr-2 text-label">
사용자 이름
</Label>
<Input
id="userName"
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
value={userName}
onChange={(e) => setUserName(e.target.value)}
placeholder="사용자 이름으로 검색"
/>
</div>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
<Button
type="button"
color="white"
className="h-[3.2rem]"
onClick={resetSearch}
>
초기화
</Button>
</form>
{tableComponent}
<DialogFooter>
<Button color="white" onClick={() => setOpen(false)}>
@@ -35,9 +35,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm",
header: "게시기간",
cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
},
},
@@ -72,7 +72,7 @@ function AddUserDialog({ onAddUsers, ...props }: AddUserDialogProps) {
</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>콘텐츠 조회</DialogTitle>
<DialogTitle>사용자 조회</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4">
<div className="card card-border !m-0 flex w-full items-center gap-2">
@@ -242,6 +242,16 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
</div>
<div className="card max-h-[calc(100vh-58rem)] overflow-auto !p-0">
<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">
<TableRow>
<TableHead>
@@ -300,6 +310,9 @@ const UserCells = memo(
jobPositionCodeData,
jobGradeCodeData,
}: UserCellsProps) => {
const [jbpsSeCd, setJbpsSeCd] = useState(user.jbpsSeCd);
const [jbgdSeCd, setJbgdSeCd] = useState(user.jbgdSeCd);
const updateMode = (index: number) => {
const mode = form.watch(`userOrganization.${index}.mode`);
if (mode === "R") {
@@ -322,17 +335,33 @@ const UserCells = memo(
<CodeSelect
codeData={jobPositionCodeData}
defaultValue={user.jbpsSeCd}
onValueChange={updateFormValue(index, "jbpsSeCd")}
onValueChange={(value) => {
setJbpsSeCd(value);
updateFormValue(index, "jbpsSeCd")(value);
}}
{...form.register(`userOrganization.${index}.jbpsSeCd`)}
/>
{!jbpsSeCd && (
<p className="pt-1 text-base text-destructive">
직위를 선택해주세요.
</p>
)}
</TableCell>
<TableCell align="center">
<CodeSelect
codeData={jobGradeCodeData}
defaultValue={user.jbgdSeCd}
onValueChange={updateFormValue(index, "jbgdSeCd")}
onValueChange={(value) => {
setJbgdSeCd(value);
updateFormValue(index, "jbgdSeCd")(value);
}}
{...form.register(`userOrganization.${index}.jbgdSeCd`)}
/>
{!jbgdSeCd && (
<p className="pt-1 text-base text-destructive">
직급을 선택해주세요.
</p>
)}
</TableCell>
<TableCell align="center">
<Input
@@ -33,9 +33,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm",
header: "게시기간",
cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
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 { ColumnDef } from "@tanstack/react-table";
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",
header: "NO",
@@ -39,7 +44,18 @@ export const columns: ColumnDef<Role>[] = [
{
id: "action",
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 { RoleApi, type Role, type RoleParams } from "@/entities/role";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { columns } from "../model/table-columns";
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
import { createColumns } from "../model/table-columns";
function RolePage() {
const router = useRouter();
const alert = useAlertStore((state) => state.setMessage);
const [keyword, setKeyword] = useState("");
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) => {
router.push(`/role/register/${row.roleId}`);
};
@@ -40,7 +48,9 @@ function RolePage() {
const { tableComponent } = useDataTable({
query,
tableOptions: {
columns,
columns: createColumns({
deleteRole,
}),
state: {
pagination,
},
@@ -36,9 +36,14 @@ export const columns: ColumnDef<Post>[] = [
accessorKey: "pstgPrdSeNm",
header: "게시기간",
cell: ({ row }) => {
const pstgPrdSeCd = row.original.pstgPrdSeCd;
const pstgPrdSeNm = row.original.pstgPrdSeNm;
const pstgBgngDt = row.original.pstgBgngDt;
const pstgEndDt = row.original.pstgEndDt;
if (pstgPrdSeCd === "99") {
return pstgPrdSeNm;
}
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
},
},
@@ -1,6 +1,11 @@
import dayjs from "dayjs";
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>[] = [
{
@@ -42,8 +47,11 @@ export const columns: ColumnDef<UserListResponse>[] = [
header: "기관/부서",
},
{
accessorKey: "userStateCodeName",
accessorKey: "userStateCode",
header: "회원상태",
cell: ({ row }) => {
return getUserStateBadge(row.original.userStateCode);
},
},
{
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: {
userId: "",
userName: "",
email: "",
password: "",
pswdHnt: "",
pswdAns: "",
mphn: "",
telno: "",
zip: "",
addr: "",
daddr: "",
laeSeCd: "",
ogdpSeCd: "",
expryYmd: "",
instSeCd: "",
enoCd: "",
},
});