diff --git a/backend-spring/src/main/java/com/erp/controller/FavoritesController.java b/backend-spring/src/main/java/com/erp/controller/FavoritesController.java new file mode 100644 index 00000000..f4e8e5b3 --- /dev/null +++ b/backend-spring/src/main/java/com/erp/controller/FavoritesController.java @@ -0,0 +1,73 @@ +package com.erp.controller; + +import com.erp.dto.ApiResponse; +import com.erp.service.FavoritesService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/favorites") +@RequiredArgsConstructor +@Slf4j +public class FavoritesController { + + private final FavoritesService favoritesService; + + /** + * GET /api/favorites/menus + * 로그인 사용자의 즐겨찾기 메뉴 목록. + */ + @GetMapping("/menus") + public ResponseEntity>>> getMyFavorites( + @RequestAttribute("user_id") String userId) { + Map params = new HashMap<>(); + params.put("user_id", userId); + return ResponseEntity.ok(ApiResponse.success( + favoritesService.getFavoriteMenuList(params), + "즐겨찾기 메뉴 조회 성공")); + } + + /** + * POST /api/favorites/menus + * 즐겨찾기 추가. body: { menu_objid, sort_order? } + */ + @PostMapping("/menus") + public ResponseEntity>> addFavorite( + @RequestAttribute("user_id") String userId, + @RequestBody Map body) { + Object menuObjid = body.get("menu_objid"); + if (menuObjid == null || String.valueOf(menuObjid).isBlank()) { + return ResponseEntity.badRequest().body(ApiResponse.error("menu_objid 필수입니다.")); + } + Map params = new HashMap<>(); + params.put("user_id", userId); + params.put("menu_objid", String.valueOf(menuObjid)); + params.put("sort_order", body.get("sort_order")); + return ResponseEntity.ok(ApiResponse.success( + favoritesService.insertFavorite(params), + "즐겨찾기 추가 성공")); + } + + /** + * DELETE /api/favorites/menus/{menuObjid} + * 즐겨찾기 제거. + */ + @DeleteMapping("/menus/{menuObjid}") + public ResponseEntity>> removeFavorite( + @RequestAttribute("user_id") String userId, + @PathVariable String menuObjid) { + Map params = new HashMap<>(); + params.put("user_id", userId); + params.put("menu_objid", menuObjid); + int affected = favoritesService.deleteFavorite(params); + Map result = new HashMap<>(); + result.put("deleted", affected); + return ResponseEntity.ok(ApiResponse.success(result, "즐겨찾기 제거 성공")); + } +} diff --git a/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java b/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java index 195f1b4e..aad1c53d 100644 --- a/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java +++ b/backend-spring/src/main/java/com/erp/migration/StartupSchemaMigrator.java @@ -91,6 +91,21 @@ public class StartupSchemaMigrator { "ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS ERP_MANAGED", "ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS DATA_TYPE", + // V020: 사용자별 메뉴 즐겨찾기 테이블. + // 메타 DB 는 Flyway V020 으로도 적용되지만 프로비저닝된 테넌트 DB 는 부팅 때 동기화. + // CREATE IF NOT EXISTS 로 멱등성 보장. + """ + CREATE TABLE IF NOT EXISTS USER_MENU_FAVORITES ( + OBJID BIGSERIAL PRIMARY KEY, + USER_ID VARCHAR(100) NOT NULL, + MENU_OBJID VARCHAR(50) NOT NULL, + SORT_ORDER INTEGER NOT NULL DEFAULT 0, + CREATED_AT TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT UQ_USER_MENU_FAVORITES UNIQUE (USER_ID, MENU_OBJID) + ) + """, + "CREATE INDEX IF NOT EXISTS IDX_USER_MENU_FAVORITES_USER ON USER_MENU_FAVORITES (USER_ID)", + // RUN_086 (1) btree_gist 확장 — USER_SUBSTITUTES 의 EXCLUDE 제약 의존성 "CREATE EXTENSION IF NOT EXISTS btree_gist", diff --git a/backend-spring/src/main/java/com/erp/service/FavoritesService.java b/backend-spring/src/main/java/com/erp/service/FavoritesService.java new file mode 100644 index 00000000..0fed9f67 --- /dev/null +++ b/backend-spring/src/main/java/com/erp/service/FavoritesService.java @@ -0,0 +1,38 @@ +package com.erp.service; + +import com.erp.common.BaseService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@Slf4j +public class FavoritesService extends BaseService { + + public List> getFavoriteMenuList(Map params) { + return sqlSession.selectList("favorites.selectFavoriteMenuList", params); + } + + @Transactional + public Map insertFavorite(Map params) { + sqlSession.insert("favorites.insertFavorite", params); + Map result = new HashMap<>(); + result.put("user_id", params.get("user_id")); + result.put("menu_objid", params.get("menu_objid")); + return result; + } + + @Transactional + public int deleteFavorite(Map params) { + return sqlSession.delete("favorites.deleteFavorite", params); + } + + public boolean exists(Map params) { + Integer cnt = sqlSession.selectOne("favorites.selectFavoriteExists", params); + return cnt != null && cnt > 0; + } +} diff --git a/backend-spring/src/main/resources/db/migration/V020__create_user_menu_favorites.sql b/backend-spring/src/main/resources/db/migration/V020__create_user_menu_favorites.sql new file mode 100644 index 00000000..4c0e5cd4 --- /dev/null +++ b/backend-spring/src/main/resources/db/migration/V020__create_user_menu_favorites.sql @@ -0,0 +1,16 @@ +-- V020: 사용자별 메뉴 즐겨찾기 테이블 +-- 로그인 사용자가 사이드바 메뉴 항목을 즐겨찾기에 등록/해제하면 한 행씩 쌓이고, +-- 사이드바 최상단 '즐겨찾기' 섹션이 이 행들을 읽어 표시한다. +-- 테넌트 DB 별로 격리 (회사마다 메뉴가 달라 cross-tenant 공용으로 묶지 않음). + +CREATE TABLE IF NOT EXISTS USER_MENU_FAVORITES ( + OBJID BIGSERIAL PRIMARY KEY, + USER_ID VARCHAR(100) NOT NULL, + MENU_OBJID VARCHAR(50) NOT NULL, + SORT_ORDER INTEGER NOT NULL DEFAULT 0, + CREATED_AT TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT UQ_USER_MENU_FAVORITES UNIQUE (USER_ID, MENU_OBJID) +); + +CREATE INDEX IF NOT EXISTS IDX_USER_MENU_FAVORITES_USER + ON USER_MENU_FAVORITES (USER_ID); diff --git a/backend-spring/src/main/resources/mapper/favorites.xml b/backend-spring/src/main/resources/mapper/favorites.xml new file mode 100644 index 00000000..959c3da6 --- /dev/null +++ b/backend-spring/src/main/resources/mapper/favorites.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + INSERT INTO USER_MENU_FAVORITES (USER_ID, MENU_OBJID, SORT_ORDER) + VALUES (#{user_id}, #{menu_objid}, COALESCE(#{sort_order}, 0)) + ON CONFLICT (USER_ID, MENU_OBJID) DO NOTHING + + + + + DELETE FROM USER_MENU_FAVORITES + WHERE USER_ID = #{user_id} + AND MENU_OBJID = #{menu_objid} + + + + + + diff --git a/frontend/app/(main)/admin/userMng/rolesList/page.tsx b/frontend/app/(main)/admin/userMng/rolesList/page.tsx index 07748cda..b6d62717 100644 --- a/frontend/app/(main)/admin/userMng/rolesList/page.tsx +++ b/frontend/app/(main)/admin/userMng/rolesList/page.tsx @@ -592,19 +592,21 @@ export default function RolesPage() { } return ( -
-
-
+
+
+

권한 관리

권한 그룹 선택 시 권한있는/없는 직원과 메뉴 권한이 로드되고, 체크 즉시 반영됩니다.

- +
+ +
{error && ( -
+

{error}