[common/features] feat: 콘텐츠 메뉴 평가, 퍼가기, 수정요청 기능 작업

This commit is contained in:
2025-07-04 18:54:58 +09:00
parent d8d4560823
commit 7d5ce5e3ec
12 changed files with 571 additions and 22 deletions
@@ -2,7 +2,7 @@
import { useQuery } from "@tanstack/react-query";
import { MenuApi } from "@/entities/menu";
import { getCookie } from "@/shared/lib";
import { useMenuStore } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { PageLayout } from "@/shared/ui/page-layout";
@@ -11,11 +11,12 @@ export default function ContentsLayout({
}: {
children: React.ReactNode;
}) {
const menuCd = getCookie("menu-code-key");
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
const { data } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, menuCd],
queryFn: () => MenuApi.getMenuInfo(menuCd!),
enabled: !!menuCd,
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, currentMenuInfo?.menuCd],
queryFn: () =>
MenuApi.getMenuInfo(currentMenuInfo?.menuCd.toString() ?? ""),
enabled: !!currentMenuInfo,
});
const menuPaths = data?.menuPath?.split(">").map((path) => path.trim());
@@ -2,7 +2,7 @@
import { MenuApi } from "@/entities/menu";
import { ENDPOINT } from "@/shared/config";
import { getCookie, useMenuStore } from "@/shared/lib";
import { useMenuStore } from "@/shared/lib";
import { PageLayout } from "@/shared/ui/page-layout";
import { useQuery } from "@tanstack/react-query";
@@ -11,13 +11,12 @@ export default function PostLayout({
}: {
children: React.ReactNode;
}) {
const bbsInfo = useMenuStore((state) => state.bbsInfo);
const menuCd = getCookie("menu-code-key");
const { bbsInfo, currentMenuInfo } = useMenuStore((state) => state);
const { data } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, menuCd],
queryFn: () => MenuApi.getMenuInfo(menuCd!),
enabled: !!menuCd,
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, currentMenuInfo?.menuCd],
queryFn: () =>
MenuApi.getMenuInfo(currentMenuInfo?.menuCd.toString() ?? ""),
enabled: !!currentMenuInfo,
});
const menuPaths = data?.menuPath?.split(">").map((path) => path.trim());
@@ -21,9 +21,11 @@ export class ContentsApiService implements ContentsService {
public async getLatestApprovedContentsInfo(
contsSn: number,
menuCd: string,
): Promise<ContentsDetail> {
const response = await client.get<ContentsDetail>(
`${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`,
{ params: { menuCd } },
);
return response.data;
}
@@ -65,6 +65,7 @@ export const contentsDetailSchema = z.object({
aprvDt: z.string(),
autzr: z.string(),
useYn: z.string(),
menuPicUserUnqIdNm: z.string().nullish(),
});
export const contentsFormSchema = contentsDetailSchema.omit({
@@ -81,7 +82,10 @@ export type ContentsVersionList = PageableList<ContentsVersion>;
export interface ContentsService {
getContentsList(params: ContentsParams): Promise<ContentsList>;
getLatestApprovedContentsInfo(contsSn: number): Promise<ContentsDetail>;
getLatestApprovedContentsInfo(
contsSn: number,
menuCd: string,
): Promise<ContentsDetail>;
getContentsVersionList(
params: ContentsVersionParams,
): Promise<ContentsVersionList>;
@@ -0,0 +1,59 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
MenuEvaluation,
MenuEvaluationDetail,
MenuEvaluationParams,
MenuEvaluationService,
MenuEvaluationCreateRequestDto,
MenuEvaluationUpdateRequestDto,
PageMenuEvaluationListResponseDto,
} from "../model/menu-evaluation-interface";
export class MenuEvaluationApiService implements MenuEvaluationService {
public async getMenuEvaluationList(
params: MenuEvaluationParams,
): Promise<PageMenuEvaluationListResponseDto> {
const response = await client.get<PageMenuEvaluationListResponseDto>(
`${ENDPOINT.CMS_SERVICE.MENU_EVALUATION}`,
{ params },
);
return response.data;
}
public async getMenuEvaluation(
menuEvlSn: number,
): Promise<MenuEvaluationDetail> {
const response = await client.get<MenuEvaluationDetail>(
`${ENDPOINT.CMS_SERVICE.MENU_EVALUATION}/${menuEvlSn}`,
);
return response.data;
}
public async createMenuEvaluation(
data: MenuEvaluationCreateRequestDto,
): Promise<MenuEvaluation> {
const response = await client.post<MenuEvaluation>(
`${ENDPOINT.CMS_SERVICE.MENU_EVALUATION}`,
data,
);
return response.data;
}
public async updateMenuEvaluation(
menuEvlSn: number,
data: MenuEvaluationUpdateRequestDto,
): Promise<MenuEvaluation> {
const response = await client.put<MenuEvaluation>(
`${ENDPOINT.CMS_SERVICE.MENU_EVALUATION}/${menuEvlSn}`,
data,
);
return response.data;
}
public async deleteMenuEvaluation(menuEvlSn: number): Promise<void> {
await client.delete(`${ENDPOINT.CMS_SERVICE.MENU_EVALUATION}/${menuEvlSn}`);
}
}
export const MenuEvaluationApi = new MenuEvaluationApiService();
@@ -0,0 +1,66 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
MenuModificationDemand,
MenuModificationDemandCreateRequestDto,
MenuModificationDemandDetail,
MenuModificationDemandParams,
MenuModificationDemandService,
MenuModificationDemandUpdateRequestDto,
PageMenuModificationDemandListResponseDto,
} from "../model/menu-modification-demand-interface";
export class MenuModificationDemandApiService
implements MenuModificationDemandService
{
public async getMenuModificationDemandList(
params: MenuModificationDemandParams,
): Promise<PageMenuModificationDemandListResponseDto> {
const response =
await client.get<PageMenuModificationDemandListResponseDto>(
`${ENDPOINT.CMS_SERVICE.MENU_MODIFICATION_DEMAND}`,
{ params },
);
return response.data;
}
public async getMenuModificationDemand(
mdfcnRcptNo: number,
): Promise<MenuModificationDemandDetail> {
const response = await client.get<MenuModificationDemandDetail>(
`${ENDPOINT.CMS_SERVICE.MENU_MODIFICATION_DEMAND}/${mdfcnRcptNo}`,
);
return response.data;
}
public async createMenuModificationDemand(
data: MenuModificationDemandCreateRequestDto,
): Promise<MenuModificationDemand> {
const response = await client.post<MenuModificationDemand>(
`${ENDPOINT.CMS_SERVICE.MENU_MODIFICATION_DEMAND}`,
data,
);
return response.data;
}
public async updateMenuModificationDemand(
mdfcnRcptNo: number,
data: MenuModificationDemandUpdateRequestDto,
): Promise<MenuModificationDemand> {
const response = await client.put<MenuModificationDemand>(
`${ENDPOINT.CMS_SERVICE.MENU_MODIFICATION_DEMAND}/${mdfcnRcptNo}`,
data,
);
return response.data;
}
public async deleteMenuModificationDemand(
mdfcnRcptNo: number,
): Promise<void> {
await client.delete(
`${ENDPOINT.CMS_SERVICE.MENU_MODIFICATION_DEMAND}/${mdfcnRcptNo}`,
);
}
}
export const MenuModificationDemandApi = new MenuModificationDemandApiService();
@@ -0,0 +1,49 @@
import type { PageableList, PageableParams } from "@/shared/model";
export interface MenuEvaluationParams extends PageableParams {
menuCd?: number;
siteId?: string;
evlIp?: string;
}
export interface MenuEvaluation {
evlSn: number;
menuCd: number;
siteId: string;
evlDt: string;
evlScr: number;
}
export interface MenuEvaluationDetail extends MenuEvaluation {
evlIp: string;
userUnqId: string;
}
export interface MenuEvaluationCreateRequestDto {
menuCd: number;
siteId: string;
evlIp?: string;
evlScr: number;
userUnqId: string;
}
export interface MenuEvaluationUpdateRequestDto {
evlScr: number;
}
export type PageMenuEvaluationListResponseDto = PageableList<MenuEvaluation>;
export interface MenuEvaluationService {
getMenuEvaluationList(
params: MenuEvaluationParams,
): Promise<PageMenuEvaluationListResponseDto>;
getMenuEvaluation(menuEvlSn: number): Promise<MenuEvaluationDetail>;
createMenuEvaluation(
data: MenuEvaluationCreateRequestDto,
): Promise<MenuEvaluation>;
updateMenuEvaluation(
menuEvlSn: number,
data: MenuEvaluationUpdateRequestDto,
): Promise<MenuEvaluation>;
deleteMenuEvaluation(menuEvlSn: number): Promise<void>;
}
@@ -0,0 +1,68 @@
import type { PageableList, PageableParams } from "@/shared/model";
import { z } from "zod";
export interface MenuModificationDemandParams extends PageableParams {
menuCd?: number;
siteId?: string;
mdfcnDmndSttusCd?: string;
}
export interface MenuModificationDemand {
mdfcnRcptNo: number;
menuCd: number;
rcptDt: string;
aplcnt: string;
aplcnYn: string;
}
export interface MenuModificationDemandDetail extends MenuModificationDemand {
mdfcnCn: string;
userUnqId: string;
aplcntEml: string;
}
export const menuModificationDemandCreateRequestDtoSchema = z.object({
menuCd: z.number(),
mdfcnCn: z.string(),
userUnqId: z.string().optional(),
aplcnt: z.string(),
aplcntEml: z.string().optional(),
});
export const menuModificationDemandUpdateRequestDtoSchema = z.object({
mdfcnCn: z.string(),
aplcntEml: z.string().optional(),
aplcnYn: z.string(),
});
export type MenuModificationDemandCreateRequestDto = z.infer<
typeof menuModificationDemandCreateRequestDtoSchema
>;
export type MenuModificationDemandUpdateRequestDto = z.infer<
typeof menuModificationDemandUpdateRequestDtoSchema
>;
export type PageMenuModificationDemandListResponseDto =
PageableList<MenuModificationDemand>;
export interface MenuModificationDemandService {
getMenuModificationDemandList(
params: MenuModificationDemandParams,
): Promise<PageMenuModificationDemandListResponseDto>;
getMenuModificationDemand(
mdfcnRcptNo: number,
): Promise<MenuModificationDemandDetail>;
createMenuModificationDemand(
data: MenuModificationDemandCreateRequestDto,
): Promise<MenuModificationDemand>;
updateMenuModificationDemand(
mdfcnRcptNo: number,
data: MenuModificationDemandUpdateRequestDto,
): Promise<MenuModificationDemand>;
deleteMenuModificationDemand(mdfcnRcptNo: number): Promise<void>;
}
@@ -1,17 +1,71 @@
"use client";
import { useEffect, useRef } from "react";
import { useQuery } from "@tanstack/react-query";
import dayjs from "dayjs";
import Image from "next/image";
import { toast } from "sonner";
import { useEffect, useRef, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useClipboard } from "@frontend-opensource/use-react-hooks";
import {
Button,
getCookie,
Label,
RadioGroup,
RadioGroupItem,
} from "@common/components";
import { CodeApi } from "@/entities/code";
import { ContentsApi } from "@/entities/content";
import { ENDPOINT } from "@/shared/config";
import { AUTH_USER_ID, ENDPOINT } from "@/shared/config";
import { useAlertStore, useMenuStore } from "@/shared/lib";
import { MenuEvaluationApi } from "../api/menu-evaluation-service";
import { MenuModificationDemandDialog } from "./menu-modification-demand-dialog";
function ContentsPage({ contsSn }: { contsSn: number }) {
const [evlScr, setEvlScr] = useState(5);
const shadowRootRef = useRef<HTMLDivElement>(null);
const { copyText, copied } = useClipboard();
const alert = useAlertStore((state) => state.setMessage);
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
const siteId = getCookie("site-id-key") ?? "";
const userUnqId = getCookie(AUTH_USER_ID) ?? "";
const { data: contentsDetail } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
queryFn: () => ContentsApi.getLatestApprovedContentsInfo(contsSn),
enabled: !!contsSn,
queryFn: () =>
ContentsApi.getLatestApprovedContentsInfo(
contsSn,
currentMenuInfo?.menuCd.toString() ?? "",
),
enabled: !!contsSn && !!currentMenuInfo,
});
const { data: koglData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "KOGLCD"],
queryFn: () => CodeApi.getCode("KOGLCD"),
select: (data) =>
data.find((code) => code.lwpoCd === contentsDetail?.koglCd),
enabled: !!contentsDetail,
});
const { data: cclData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "CCLCD"],
queryFn: () => CodeApi.getCode("CCLCD"),
select: (data) =>
data.find((code) => code.lwpoCd === contentsDetail?.cclCd),
enabled: !!contentsDetail,
});
const { mutate: menuEvaluation } = useMutation({
mutationFn: (evlScr: number) =>
MenuEvaluationApi.createMenuEvaluation({
menuCd: currentMenuInfo?.menuCd ?? 0,
siteId,
evlScr,
userUnqId,
}),
onSuccess: () => {
alert("게시물 평가가 완료되었습니다.");
},
});
useEffect(() => {
@@ -28,10 +82,81 @@ function ContentsPage({ contsSn }: { contsSn: number }) {
shadowRoot.appendChild(contentElement);
}, [contentsDetail?.contsCn]);
useEffect(() => {
if (copied) toast.success("URL이 클립보드에 복사되었습니다.");
}, [copied]);
return (
<div className="card card-border !mt-6 !p-6">
<div ref={shadowRootRef} />
</div>
<>
<div className="card card-border !mt-6 !p-6">
<div ref={shadowRootRef} />
</div>
<div className="flex">
<div className="card card-border flex flex-1 items-center gap-2">
<div className="rounded-sm border border-border bg-accent px-8 py-4">
<RadioGroup
value={evlScr.toString()}
onValueChange={(value) => setEvlScr(parseInt(value))}
className="flex items-center space-x-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="5" id="evl5" />
<Label htmlFor="evl5">매우 만족 😊</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="4" id="evl4" />
<Label htmlFor="evl4">만족 🙂</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="3" id="evl3" />
<Label htmlFor="evl3">보통 😐</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="2" id="evl2" />
<Label htmlFor="evl2">불만족 🙁</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="1" id="evl1" />
<Label htmlFor="evl1">매우 불만족 😞</Label>
</div>
</RadioGroup>
</div>
<Button
size="lg"
color="white"
onClick={() => menuEvaluation(evlScr)}
>
평가하기
</Button>
</div>
<div className="card card-border flex flex-1 items-center gap-4">
<div className="rounded-sm border border-border bg-accent px-8 py-4">
최종수정일: {dayjs(contentsDetail?.aprvDt).format("YYYY-MM-DD")} |
담당자: {contentsDetail?.menuPicUserUnqIdNm}
</div>
<Image
src={`/images/${cclData?.unit}`}
alt="CCL"
width={114}
height={40}
/>
<Image
src={`/images/${koglData?.unit}`}
alt="CCL"
width={140}
height={40}
/>
<Button
size="lg"
color="emerald"
onClick={() => copyText(window.location.href)}
>
퍼가기
</Button>
<MenuModificationDemandDialog menuCd={currentMenuInfo?.menuCd} />
</div>
</div>
</>
);
}
@@ -0,0 +1,173 @@
import { memo, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useMutation } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Input,
DialogTrigger,
Textarea,
} from "@common/components";
import { useAlertStore } from "@/shared/lib";
import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form";
import { MenuModificationDemandApi } from "../api/menu-modification-demand-service";
import {
menuModificationDemandCreateRequestDtoSchema,
type MenuModificationDemandCreateRequestDto,
} from "../model/menu-modification-demand-interface";
type MenuModificationDemandDialogProps = {
menuCd?: number;
};
const MenuModificationDemandDialog = memo(
({ menuCd }: MenuModificationDemandDialogProps) => {
const [open, setOpen] = useState(false);
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createMenuModificationDemand } = useMutation({
mutationFn: (data: MenuModificationDemandCreateRequestDto) =>
MenuModificationDemandApi.createMenuModificationDemand(data),
onSuccess: () => {
alert("메뉴 수정 요청이 등록되었습니다.", () => setOpen(false));
},
});
const form = useForm<MenuModificationDemandCreateRequestDto>({
resolver: zodResolver(menuModificationDemandCreateRequestDtoSchema),
defaultValues: {
menuCd: menuCd ?? 0,
mdfcnCn: "",
aplcnt: "",
aplcntEml: "",
},
});
const onSubmit = async (data: MenuModificationDemandCreateRequestDto) => {
if (await confirm("메뉴 수정 요청을 등록하시겠습니까?")) {
createMenuModificationDemand(data);
}
};
useEffect(() => {
if (!open) form.reset();
}, [open]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button size="lg" color="white">
수정요청
</Button>
</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>메뉴 수정 요청</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Table variant="secondary">
<colgroup>
<col width="15%" />
<col width="85%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>신청자</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="aplcnt"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="신청자명을 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>이메일</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="aplcntEml"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="이메일을 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>요청 내용</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="mdfcnCn"
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="요청 내용을 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
<DialogFooter className="!justify-center">
<Button size="lg" type="submit">
요청하기
</Button>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
},
);
MenuModificationDemandDialog.displayName = "MenuModificationDemandDialog";
export { MenuModificationDemandDialog };
@@ -28,6 +28,8 @@ export const ENDPOINT = {
MENUS: "/cms-service/api/v1/menus",
MENU_ROLES: "/cms-service/api/v1/menu-roles",
MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts",
MENU_EVALUATION: "/cms-service/api/v1/menu-evls",
MENU_MODIFICATION_DEMAND: "/cms-service/api/v1/menu-mdfcn-dmnds",
CONTENTS: "/cms-service/api/v1/contents",
CONTENTS_DETAILS: "/cms-service/api/v1/contents-details",
ORGANIZATIONS: "/cms-service/api/v1/ognzs",
@@ -41,6 +43,7 @@ export const ENDPOINT = {
BBS: "/board-service/api/v1/bbs",
POSTS_USER: "/board-service/api/v1/posts/user",
POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts",
POST_EVALUATION: "/board-service/api/v1/pstEvl",
},
};
@@ -15,8 +15,8 @@ type PostActionProps = {
function PostAction({ registerId, children, handleDelete }: PostActionProps) {
const router = useRouter();
const pathname = usePathname();
const { copyText, copied } = useClipboard();
const { confirm } = useConfirm();
const { copyText, copied } = useClipboard();
const bbsInfo = useMenuStore((state) => state.bbsInfo);
const userInfo = useUserStore((state) => state.userInfo);