From 37d93d82b1b061406442631b530cb28589f02041 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 4 Mar 2026 14:01:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(screen):=20PC/POP=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20(excludePop=20=ED=95=84=ED=84=B0)=20PC=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EA=B4=80=EB=A6=AC=EC=97=90=EC=84=9C=20POP=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=20=ED=99=94=EB=A9=B4=EA=B3=BC=20=EA=B7=B8=EB=A3=B9?= =?UTF-8?q?=EC=9D=B4=20=ED=95=A8=EA=BB=98=20=ED=91=9C=EC=8B=9C=EB=90=98?= =?UTF-8?q?=EC=96=B4=20=ED=98=BC=EB=8F=99=EC=9D=84=20=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=EB=A5=BC=20=ED=95=B4=EA=B2=B0=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=B4=20excludePop=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=EB=8F=84=EC=9E=85=ED=95=9C=EB=8B=A4.=20[?= =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C]=20-=20getScreensByCompany:=20NOT?= =?UTF-8?q?=20EXISTS=20=EC=84=9C=EB=B8=8C=EC=BF=BC=EB=A6=AC=EB=A1=9C=20scr?= =?UTF-8?q?een=5Flayouts=5Fpop=EC=97=90=20=20=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=EC=9D=B4=20=EC=9E=88=EB=8A=94=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=A0=9C=EC=99=B8,=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=20=EB=B3=84=EC=B9=AD=20sd=EB=A1=9C=20=ED=86=B5=EC=9D=BC=20-=20?= =?UTF-8?q?getScreenGroups:=20hierarchy=5Fpath=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20POP=20=EA=B7=B8=EB=A3=B9=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8=20=20=20(hierarchy=5Fpath=20IS=20NULL=20OR=20NOT=20LI?= =?UTF-8?q?KE=20'POP/%')=20-=20=EB=91=90=20API=20=EB=AA=A8=EB=91=90=20excl?= =?UTF-8?q?udePop=20=EB=AF=B8=EC=A0=84=EB=8B=AC=20=EC=8B=9C=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20=EB=8F=99=EC=9E=91=20100%=20=EC=9C=A0=EC=A7=80=20[?= =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C]=20-=20screenApi.?= =?UTF-8?q?getScreens,=20getScreenGroups=EC=97=90=20excludePop=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80=20-=20PC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80,=20ScreenGroupTreeView,=20ScreenList=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=20=20excludePop:=20true=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/screenGroupController.ts | 7 ++++- .../controllers/screenManagementController.ts | 5 ++-- .../src/services/screenManagementService.ts | 27 ++++++++++++------- .../admin/screenMng/screenMngList/page.tsx | 2 +- .../components/screen/ScreenGroupTreeView.tsx | 2 +- frontend/components/screen/ScreenList.tsx | 2 +- frontend/lib/api/screen.ts | 1 + frontend/lib/api/screenGroup.ts | 2 ++ 8 files changed, 32 insertions(+), 16 deletions(-) diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index df975d8f..e1a30665 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -20,7 +20,7 @@ const pool = getPool(); export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) => { try { const companyCode = req.user?.companyCode || "*"; - const { page = 1, size = 20, searchTerm } = req.query; + const { page = 1, size = 20, searchTerm, excludePop } = req.query; const offset = (parseInt(page as string) - 1) * parseInt(size as string); let whereClause = "WHERE 1=1"; @@ -34,6 +34,11 @@ export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) paramIndex++; } + // POP 그룹 제외 (PC 화면관리용) + if (excludePop === "true") { + whereClause += ` AND (hierarchy_path IS NULL OR (hierarchy_path NOT LIKE 'POP/%' AND hierarchy_path != 'POP'))`; + } + // 검색어 필터링 if (searchTerm) { whereClause += ` AND (group_name ILIKE $${paramIndex} OR group_code ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`; diff --git a/backend-node/src/controllers/screenManagementController.ts b/backend-node/src/controllers/screenManagementController.ts index afd03461..a6e2a57e 100644 --- a/backend-node/src/controllers/screenManagementController.ts +++ b/backend-node/src/controllers/screenManagementController.ts @@ -6,7 +6,7 @@ import { AuthenticatedRequest } from "../types/auth"; export const getScreens = async (req: AuthenticatedRequest, res: Response) => { try { const userCompanyCode = (req.user as any).companyCode; - const { page = 1, size = 20, searchTerm, companyCode } = req.query; + const { page = 1, size = 20, searchTerm, companyCode, excludePop } = req.query; // 쿼리 파라미터로 companyCode가 전달되면 해당 회사의 화면 조회 (최고 관리자 전용) // 아니면 현재 사용자의 companyCode 사용 @@ -24,7 +24,8 @@ export const getScreens = async (req: AuthenticatedRequest, res: Response) => { targetCompanyCode, parseInt(page as string), parseInt(size as string), - searchTerm as string // 검색어 전달 + searchTerm as string, + { excludePop: excludePop === "true" }, ); res.json({ diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 0cf2da7e..20b54d50 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -108,42 +108,49 @@ export class ScreenManagementService { companyCode: string, page: number = 1, size: number = 20, - searchTerm?: string, // 검색어 추가 + searchTerm?: string, + options?: { excludePop?: boolean }, ): Promise> { const offset = (page - 1) * size; // WHERE 절 동적 생성 - const whereConditions: string[] = ["is_active != 'D'"]; + const whereConditions: string[] = ["sd.is_active != 'D'"]; const params: any[] = []; if (companyCode !== "*") { - whereConditions.push(`company_code = $${params.length + 1}`); + whereConditions.push(`sd.company_code = $${params.length + 1}`); params.push(companyCode); } - // 검색어 필터링 추가 (화면명, 화면 코드, 테이블명 검색) if (searchTerm && searchTerm.trim() !== "") { whereConditions.push(`( - screen_name ILIKE $${params.length + 1} OR - screen_code ILIKE $${params.length + 1} OR - table_name ILIKE $${params.length + 1} + sd.screen_name ILIKE $${params.length + 1} OR + sd.screen_code ILIKE $${params.length + 1} OR + sd.table_name ILIKE $${params.length + 1} )`); params.push(`%${searchTerm.trim()}%`); } + // POP 화면 제외 필터: screen_layouts_pop에 레이아웃이 있는 화면 제외 + if (options?.excludePop) { + whereConditions.push( + `NOT EXISTS (SELECT 1 FROM screen_layouts_pop slp WHERE slp.screen_id = sd.screen_id)` + ); + } + const whereSQL = whereConditions.join(" AND "); // 페이징 쿼리 (Raw Query) const [screens, totalResult] = await Promise.all([ query( - `SELECT * FROM screen_definitions + `SELECT sd.* FROM screen_definitions sd WHERE ${whereSQL} - ORDER BY created_date DESC + ORDER BY sd.created_date DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, [...params, size, offset], ), query<{ count: string }>( - `SELECT COUNT(*)::text as count FROM screen_definitions + `SELECT COUNT(*)::text as count FROM screen_definitions sd WHERE ${whereSQL}`, params, ), diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 2104c711..7a6705c4 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -38,7 +38,7 @@ export default function ScreenManagementPage() { const loadScreens = useCallback(async () => { try { setLoading(true); - const result = await screenApi.getScreens({ page: 1, size: 1000, searchTerm: "" }); + const result = await screenApi.getScreens({ page: 1, size: 1000, searchTerm: "", excludePop: true }); // screenApi.getScreens는 { data: ScreenDefinition[], total, page, size, totalPages } 형태 반환 if (result.data && result.data.length > 0) { setScreens(result.data); diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index 1aa47f0d..9b854da1 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -1011,7 +1011,7 @@ export function ScreenGroupTreeView({ const loadGroupsData = async () => { try { setLoading(true); - const response = await getScreenGroups({ size: 1000 }); // 모든 그룹 가져오기 + const response = await getScreenGroups({ size: 1000, excludePop: true }); if (response.success && response.data) { setGroups(response.data); diff --git a/frontend/components/screen/ScreenList.tsx b/frontend/components/screen/ScreenList.tsx index 3723b5f1..9ccb4e9d 100644 --- a/frontend/components/screen/ScreenList.tsx +++ b/frontend/components/screen/ScreenList.tsx @@ -223,7 +223,7 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr const loadGroups = async () => { try { setLoadingGroups(true); - const response = await getScreenGroups(); + const response = await getScreenGroups({ excludePop: true }); if (response.success && response.data) { setGroups(response.data); } diff --git a/frontend/lib/api/screen.ts b/frontend/lib/api/screen.ts index a7159ac0..31c5e6f8 100644 --- a/frontend/lib/api/screen.ts +++ b/frontend/lib/api/screen.ts @@ -16,6 +16,7 @@ export const screenApi = { size?: number; companyCode?: string; searchTerm?: string; + excludePop?: boolean; }): Promise> => { const response = await apiClient.get("/screen-management/screens", { params }); const raw = response.data || {}; diff --git a/frontend/lib/api/screenGroup.ts b/frontend/lib/api/screenGroup.ts index f3883240..6dff3433 100644 --- a/frontend/lib/api/screenGroup.ts +++ b/frontend/lib/api/screenGroup.ts @@ -115,12 +115,14 @@ export async function getScreenGroups(params?: { page?: number; size?: number; searchTerm?: string; + excludePop?: boolean; }): Promise> { try { const queryParams = new URLSearchParams(); if (params?.page) queryParams.append("page", params.page.toString()); if (params?.size) queryParams.append("size", params.size.toString()); if (params?.searchTerm) queryParams.append("searchTerm", params.searchTerm); + if (params?.excludePop) queryParams.append("excludePop", "true"); const response = await apiClient.get(`/screen-groups/groups?${queryParams.toString()}`); return response.data;