# Pipeline: Spring Boot Rebuild (구조 단순화 + 버그 수정)

## config
- max_retries: 5
- parallel: true
- timeout: 15m
- design_first: false
- model: sonnet
- max_concurrent: 5

## tasks

# ============================================================
# Phase 1: 설정 + 인프라 (task-1 ~ task-3)
# ============================================================

### task-1: application.yml + Jackson 설정 [backend]
- depends: none
- done_when: application.yml에 write-numbers-as-strings + null 포함 설정 완료
- files: backend-spring/src/main/resources/application.yml
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    application.yml의 spring 섹션에 아래 설정 추가:
    spring:
      jackson:
        generator:
          write-numbers-as-strings: true
        default-property-inclusion: always

    1) write-numbers-as-strings: 모든 JSON 숫자를 문자열로 직렬화.
       로우코드 프론트가 모든 값을 String으로 기대하기 때문에 필수.
    2) default-property-inclusion: always: null 값도 JSON 키에 포함.
       Node는 null이라도 키를 내려주는데 Spring은 기본적으로 생략함.
       프론트에서 특정 키 존재 여부를 체크하는 코드가 있으므로 필수.

    기존 설정은 건드리지 말고 jackson 섹션만 추가할 것.

    주의: default-property-inclusion은 Map<String, Object>에는 적용되지 않을 수 있음.
    그 경우 ObjectMapper Bean을 직접 설정하는 Config 클래스 필요:
    @Configuration
    public class JacksonConfig {
        @Bean
        public ObjectMapper objectMapper() {
            ObjectMapper mapper = new ObjectMapper();
            mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
            mapper.configure(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS, true);
            return mapper;
        }
    }

### task-2: SqlSessionTemplate 설정 + BaseService 생성 [backend]
- depends: none
- done_when: BaseService에서 sqlSession 사용 가능
- files: backend-spring/src/main/java/com/erp/common/BaseService.java, backend-spring/src/main/java/com/erp/config/MyBatisConfig.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    Duckil 구조를 참고한 BaseService 생성.
    모든 Service가 상속받아 sqlSession을 사용.

    MyBatisConfig.java:
    @Configuration
    public class MyBatisConfig {
        @Bean
        public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }

    BaseService.java (Duckil의 BaseService와 동일한 역할):
    public abstract class BaseService {
        @Autowired
        protected SqlSessionTemplate sqlSession;
    }

    사용 예시 (Duckil과 동일한 호출 방식):
    List<Map<String, Object>> resultList = sqlSession.selectList("admin.getMenuList", paramMap);
    int result = sqlSession.insert("admin.insertMenu", paramMap);
    int result = sqlSession.update("admin.updateMenu", paramMap);
    int result = sqlSession.delete("admin.deleteMenu", paramMap);

    Duckil과의 차이: close() 호출 불필요 (Spring 자동 관리), @Transactional 그대로 동작.

### task-3: OGNL 작은따옴표 일괄 수정 스크립트 [backend]
- depends: none
- done_when: 모든 XML test="" 안 작은따옴표가 큰따옴표로 변경
- files: backend-spring/src/main/resources/mapper/*.xml
- test: cd backend-spring && grep -r "test=\"[^\"]*!= '[^'][^\"]*\"" src/main/resources/mapper/ | wc -l | xargs test 0 -eq
- context: |
    모든 Mapper XML의 MyBatis OGNL test 속성에서 작은따옴표 문자열 비교를 큰따옴표로 변경.
    비어있지 않은 문자열 비교만 대상 (!= '' 은 제외).

    변경 전: <if test="companyCode != null and companyCode != '*'">
    변경 후: <if test='companyCode != null and companyCode != "*"'>

    변경 전: <when test="userType == 'SUPER_ADMIN' and companyCode == '*'">
    변경 후: <when test='userType == "SUPER_ADMIN" and companyCode == "*"'>

    대상: test="" 속성 안의 != 'xxx' 또는 == 'xxx' 패턴 (xxx가 비어있지 않은 것만)
    주의: SQL 본문의 != '*' 은 건드리지 말 것 (OGNL이 아닌 SQL이므로 정상)

# ============================================================
# Phase 2: Auth/JWT 재구현 (task-4 ~ task-6)
# ============================================================

### task-4: AuthMapper XML 재작성 [backend]
- depends: task-3
- done_when: selectUserInfo가 Node와 동일한 필드 반환
- files: backend-spring/src/main/resources/mapper/AuthMapper.xml
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- ref_files: backend-node/src/services/authService.ts
- context: |
    AuthMapper.xml의 selectUserInfo 쿼리를 Node authService.ts 138~143행과 동일하게 수정.

    반드시 포함할 컬럼:
    SELECT sabun, user_id, user_name, user_name_eng, user_name_cn,
           dept_code, dept_name, position_code, position_name,
           email, tel, cell_phone, user_type, user_type_name,
           partner_objid, company_code, locale, photo
    FROM user_info
    WHERE UPPER(user_id) = UPPER(#{userId})

    추가 쿼리:
    - selectUserAuthNames: SELECT am.auth_name FROM authority_master am JOIN authority_sub_user asu ON am.objid = asu.master_objid WHERE asu.user_id = #{userId} AND am.status = 'active'
    - selectCompanyName: SELECT company_name FROM company_mng WHERE company_code = #{companyCode}

    XML namespace를 "auth"로 변경 (Duckil 스타일):
    <mapper namespace="auth">

### task-5: JwtTokenProvider 재구현 [backend]
- depends: none
- done_when: JWT 페이로드가 Node와 동일
- files: backend-spring/src/main/java/com/erp/security/JwtTokenProvider.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- ref_files: backend-node/src/utils/jwtUtils.ts
- context: |
    JWT 토큰 생성 시 Node.js와 동일한 클레임을 포함해야 함.

    Node JWT 페이로드 (jwtUtils.ts 참고):
    {
      userId: "wace",
      userName: "관리자",
      deptName: "해외영업부",
      companyCode: "*",
      companyName: "공통",
      userType: "SUPER_ADMIN",
      iat: ...,
      exp: ...,
      aud: "PMS-Users",
      iss: "PMS-System"
    }

    현재 Spring JWT (잘못됨):
    { sub: "wace", companyCode: "ILSHIN", role: "USER" }

    수정사항:
    1. sub 대신 userId 클레임 사용
    2. role 대신 userType 클레임 사용
    3. userName, deptName, companyName 클레임 추가
    4. audience("PMS-Users"), issuer("PMS-System") 설정
    5. generateToken 메서드 파라미터를 Map<String, Object> personBean으로 변경

    extractUserId는 "userId" 클레임에서 추출.
    extractCompanyCode는 "companyCode" 클레임에서 추출.
    extractUserType는 "userType" 클레임에서 추출 (기존 extractRole 대체).

### task-6: AuthService + AuthController 재구현 [backend]
- depends: task-2, task-4, task-5
- done_when: /api/auth/login 응답이 Node와 동일
- files: backend-spring/src/main/java/com/erp/service/AuthService.java, backend-spring/src/main/java/com/erp/controller/AuthController.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- api_test: $API_POST(/api/auth/login, {"userId":"wace","password":"qlalfqjsgh11"}) && $EXPECT_SUCCESS
- ref_files: backend-node/src/services/authService.ts, backend-node/src/controllers/authController.ts
- context: |
    AuthService를 BaseService 상속으로 변경, sqlSession 직접 사용.
    Mapper Interface(AuthMapper.java) 사용하지 않음.

    로그인 로직 (Node authService.ts 참고):
    1. sqlSession.selectOne("auth.selectUserInfo", params) → user_info 조회
    2. PasswordEncoder.matches()로 비밀번호 검증
    3. 마스터 패스워드 체크 (환경변수로 분리, 하드코딩 금지)
    4. sqlSession.selectList("auth.selectUserAuthNames", params) → 권한 목록
    5. sqlSession.selectOne("auth.selectCompanyName", params) → 회사명
    6. companyCode = userInfo.company_code (DB값 그대로, 디폴트 하드코딩 제거)
    7. userType = userInfo.user_type (DB값 그대로)
    8. PersonBean 구성 (Node authService.ts 191~219행과 동일한 필드)
    9. JwtTokenProvider.generateToken(personBean)
    10. firstMenuPath 계산 로직 구현

    로그인 응답 형식 (Node와 동일):
    {
      success: true,
      message: "로그인 성공",
      data: {
        token: "...",
        userInfo: {
          userId, userName, deptName, companyCode, userType, companyName, ...
        },
        firstMenuPath: "/screens/106",
        popLandingPath: null
      }
    }

    JwtAuthenticationFilter도 수정:
    - extractUserId → "userId" 클레임
    - extractRole → "userType" 클레임
    - RequestAttribute에 companyCode, userId, userType 주입

# ============================================================
# Phase 3: Mapper Interface 삭제 + Service 리팩토링 (task-7 ~ task-16)
# 96개 Service를 10묶음으로 처리
# ============================================================

### task-7: Admin/Auth 관련 Service 리팩토링 (7개) [backend]
- depends: task-2
- done_when: AdminService, RoleService, DepartmentService, CompanyManagementService, SystemNoticeService, AuditLogService, ApprovalService가 sqlSession 직접 사용
- files: backend-spring/src/main/java/com/erp/service/AdminService.java, backend-spring/src/main/java/com/erp/service/RoleService.java, backend-spring/src/main/java/com/erp/service/DepartmentService.java, backend-spring/src/main/java/com/erp/service/CompanyManagementService.java, backend-spring/src/main/java/com/erp/service/SystemNoticeService.java, backend-spring/src/main/java/com/erp/service/AuditLogService.java, backend-spring/src/main/java/com/erp/service/ApprovalService.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    모든 Service를 BaseService 상속으로 변경하고, Mapper Interface 주입을 제거.
    sqlSession으로 직접 XML 호출하는 Duckil 스타일로 변환.

    변경 전:
    @Service
    @RequiredArgsConstructor
    public class AdminService {
        private final AdminMapper adminMapper;
        public List<Map<String, Object>> getAdminMenuList(Map<String, Object> params) {
            return adminMapper.selectAdminMenuList(params);
        }
    }

    변경 후 (Duckil 스타일):
    @Service
    public class AdminService extends BaseService {
        public List<Map<String, Object>> getAdminMenuList(Map<String, Object> params) {
            return sqlSession.selectList("admin.selectAdminMenuList", params);
        }
    }

    규칙:
    - extends BaseService (sqlSession 자동 주입)
    - @RequiredArgsConstructor 제거 (Mapper 주입 없으므로)
    - 다른 Service 주입이 필요하면 @Autowired 필드 사용
    - Mapper Interface import 제거
    - 네임스페이스는 Duckil 스타일 소문자: "admin", "role", "department" 등
    - @Transactional 은 그대로 유지
    - @Slf4j 그대로 유지

    ★ JdbcTemplate 사용 Service 주의사항:
    JdbcTemplate을 사용하는 Service는 JdbcTemplate 주입을 유지해야 함.
    이 태스크에는 해당 없음.

    ★ 다른 Service 주입 현황 (이 태스크):
    - AdminService: CommonService 주입 → @Autowired CommonService commonService; 유지
    - 나머지: 다른 Service 주입 없음

    ★ 여러 Mapper를 주입받는 Service 주의:
    이 태스크에는 해당 없음.

    ★ Mapper가 없는 유틸리티 Service:
    AiAssistantProxyService, ExternalCallService는 Mapper를 사용하지 않음.
    이 Service들은 BaseService 상속 불필요, 그대로 유지.

### task-8: Table/Entity 관련 Service 리팩토링 (8개) [backend]
- depends: task-2
- done_when: 컴파일 성공
- files: backend-spring/src/main/java/com/erp/service/TableManagementService.java, backend-spring/src/main/java/com/erp/service/EntityJoinService.java, backend-spring/src/main/java/com/erp/service/EntityReferenceService.java, backend-spring/src/main/java/com/erp/service/EntitySearchService.java, backend-spring/src/main/java/com/erp/service/DdlService.java, backend-spring/src/main/java/com/erp/service/TableHistoryService.java, backend-spring/src/main/java/com/erp/service/TableCategoryValueService.java, backend-spring/src/main/java/com/erp/service/DbTypeCategoryService.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    task-7과 동일한 변환 규칙 적용.
    BaseService 상속, Mapper 제거, sqlSession 직접 사용.

### task-9: Screen/Layout 관련 Service 리팩토링 (7개) [backend]
- depends: task-2
- done_when: 컴파일 성공
- files: backend-spring/src/main/java/com/erp/service/ScreenManagementService.java, backend-spring/src/main/java/com/erp/service/ScreenStandardService.java, backend-spring/src/main/java/com/erp/service/ScreenFileService.java, backend-spring/src/main/java/com/erp/service/ScreenGroupService.java, backend-spring/src/main/java/com/erp/service/ScreenEmbeddingService.java, backend-spring/src/main/java/com/erp/service/LayoutService.java, backend-spring/src/main/java/com/erp/service/WebTypeStandardService.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    task-7과 동일한 변환 규칙 적용.

### task-10: Dataflow/Flow 관련 Service 리팩토링 (9개) [backend]
- depends: task-2
- done_when: 컴파일 성공
- files: backend-spring/src/main/java/com/erp/service/DataflowService.java, backend-spring/src/main/java/com/erp/service/DataflowDiagramService.java, backend-spring/src/main/java/com/erp/service/DataflowExecutionService.java, backend-spring/src/main/java/com/erp/service/FlowService.java, backend-spring/src/main/java/com/erp/service/FlowExternalDbConnectionService.java, backend-spring/src/main/java/com/erp/service/ButtonDataflowService.java, backend-spring/src/main/java/com/erp/service/TestButtonDataflowService.java, backend-spring/src/main/java/com/erp/service/NodeFlowService.java, backend-spring/src/main/java/com/erp/service/NodeExternalConnectionService.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    task-7과 동일한 변환 규칙 적용.
    ★ JdbcTemplate 사용 Service (유지 필요):
    - DataflowExecutionService: JdbcTemplate 유지, TableManagementService 주입 유지
    - FlowExternalDbConnectionService: JdbcTemplate 유지
    - NodeFlowService: JdbcTemplate 유지, AuditLogService 주입 유지
    - NodeExternalConnectionService: JdbcTemplate 유지
    ★ 크로스 Service 주입:
    - DataflowDiagramService: CommonService 주입
    - ButtonDataflowService: DataflowDiagramService 주입
    - FlowService: CommonService 주입

### task-11: CommonCode/Config 관련 Service 리팩토링 (8개) [backend]
- depends: task-2
- done_when: 컴파일 성공
- files: backend-spring/src/main/java/com/erp/service/CommonCodeService.java, backend-spring/src/main/java/com/erp/service/MultilangService.java, backend-spring/src/main/java/com/erp/service/ComponentStandardService.java, backend-spring/src/main/java/com/erp/service/TemplateStandardService.java, backend-spring/src/main/java/com/erp/service/ButtonActionStandardService.java, backend-spring/src/main/java/com/erp/service/DynamicFormService.java, backend-spring/src/main/java/com/erp/service/CategoryTreeService.java, backend-spring/src/main/java/com/erp/service/CommonService.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    task-7과 동일한 변환 규칙 적용.
    CommonService는 다른 Service에서 주입받아 사용될 수 있으므로 주의.

### task-12: Finance/Tax 관련 Service 리팩토링 (7개) [backend]
- depends: task-2
- done_when: 컴파일 성공
- files: backend-spring/src/main/java/com/erp/service/TaxInvoiceService.java, backend-spring/src/main/java/com/erp/service/BomService.java, backend-spring/src/main/java/com/erp/service/ProductionService.java, backend-spring/src/main/java/com/erp/service/SalesReportService.java, backend-spring/src/main/java/com/erp/service/AnalyticsReportService.java, backend-spring/src/main/java/com/erp/service/DeliveryService.java, backend-spring/src/main/java/com/erp/service/PackagingService.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    task-7과 동일한 변환 규칙 적용.

### task-13: External/Connection 관련 Service 리팩토링 (6개) [backend]
- depends: task-2
- done_when: 컴파일 성공
- files: backend-spring/src/main/java/com/erp/service/ExternalDbConnectionService.java, backend-spring/src/main/java/com/erp/service/ExternalRestApiConnectionService.java, backend-spring/src/main/java/com/erp/service/ExternalCallService.java, backend-spring/src/main/java/com/erp/service/ExternalCallConfigService.java, backend-spring/src/main/java/com/erp/service/MultiConnectionService.java, backend-spring/src/main/java/com/erp/service/OpenApiProxyService.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    task-7과 동일한 변환 규칙 적용.

### task-14: Batch/Schedule/기타 관련 Service 리팩토링 (10개) [backend]
- depends: task-2
- done_when: 컴파일 성공
- files: backend-spring/src/main/java/com/erp/service/BatchService.java, backend-spring/src/main/java/com/erp/service/BatchManagementService.java, backend-spring/src/main/java/com/erp/service/BatchExecutionLogService.java, backend-spring/src/main/java/com/erp/service/ScheduleService.java, backend-spring/src/main/java/com/erp/service/NumberingRuleService.java, backend-spring/src/main/java/com/erp/service/ProcessWorkStandardService.java, backend-spring/src/main/java/com/erp/service/CodeMergeService.java, backend-spring/src/main/java/com/erp/service/FileService.java, backend-spring/src/main/java/com/erp/service/TodoService.java, backend-spring/src/main/java/com/erp/service/AiAssistantProxyService.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    task-7과 동일한 변환 규칙 적용.
    ★ JdbcTemplate 사용: CodeMergeService (유지 필요)
    ★ Mapper 없는 Service: AiAssistantProxyService → BaseService 상속 불필요, 그대로 유지
    ★ 여러 Mapper 주입: BatchService → BatchMapper + ExternalDbConnectionMapper → sqlSession으로 통합
    ★ 크로스 Service 주입:
    - BatchManagementService: BatchService, ExternalDbConnectionService, CommonService
    - BatchService: CommonService

### task-15: Mail/Cascading 관련 Service 리팩토링 (11개) [backend]
- depends: task-2
- done_when: 컴파일 성공
- files: backend-spring/src/main/java/com/erp/service/MailAccountFileService.java, backend-spring/src/main/java/com/erp/service/MailReceiveBasicService.java, backend-spring/src/main/java/com/erp/service/MailSendSimpleService.java, backend-spring/src/main/java/com/erp/service/MailSentHistoryService.java, backend-spring/src/main/java/com/erp/service/MailTemplateFileService.java, backend-spring/src/main/java/com/erp/service/CascadingRelationService.java, backend-spring/src/main/java/com/erp/service/CascadingAutoFillService.java, backend-spring/src/main/java/com/erp/service/CascadingConditionService.java, backend-spring/src/main/java/com/erp/service/CascadingMutualExclusionService.java, backend-spring/src/main/java/com/erp/service/CascadingHierarchyService.java, backend-spring/src/main/java/com/erp/service/CategoryValueCascadingService.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    task-7과 동일한 변환 규칙 적용.
    ★ 여러 Mapper 주입 Service:
    - MailSendSimpleService: MailSendSimpleMapper + MailSentHistoryMapper + MailTemplateFileMapper
      → sqlSession으로 통합. namespace를 각각 "mailSendSimple", "mailSentHistory", "mailTemplateFile"로 호출
    ★ JdbcTemplate 사용 (전부 유지 필요):
    - CascadingRelationService, CascadingAutoFillService, CascadingConditionService
    - CascadingMutualExclusionService, CascadingHierarchyService, CategoryValueCascadingService
    ★ 크로스 Service/Mapper 주입:
    - CascadingConditionService: CommonService + CascadingRelationMapper
      → CascadingRelationMapper 주입을 제거하고 sqlSession.selectList("cascadingRelation.xxx") 로 변경

### task-16: Logistics/Report/기타 관련 Service 리팩토링 (22개) [backend]
- depends: task-2
- done_when: 컴파일 성공
- files: backend-spring/src/main/java/com/erp/service/ShippingPlanService.java, backend-spring/src/main/java/com/erp/service/ShippingOrderService.java, backend-spring/src/main/java/com/erp/service/BookingService.java, backend-spring/src/main/java/com/erp/service/DriverService.java, backend-spring/src/main/java/com/erp/service/VehicleService.java, backend-spring/src/main/java/com/erp/service/VehicleTripService.java, backend-spring/src/main/java/com/erp/service/YardLayoutService.java, backend-spring/src/main/java/com/erp/service/DigitalTwinService.java, backend-spring/src/main/java/com/erp/service/DashboardService.java, backend-spring/src/main/java/com/erp/service/ReportService.java, backend-spring/src/main/java/com/erp/service/BarcodeLabelService.java, backend-spring/src/main/java/com/erp/service/MapDataService.java, backend-spring/src/main/java/com/erp/service/ExcelMappingService.java, backend-spring/src/main/java/com/erp/service/RiskAlertService.java, backend-spring/src/main/java/com/erp/service/PopActionService.java, backend-spring/src/main/java/com/erp/service/PopProductionService.java, backend-spring/src/main/java/com/erp/service/WorkHistoryService.java, backend-spring/src/main/java/com/erp/service/MoldService.java, backend-spring/src/main/java/com/erp/service/DesignService.java, backend-spring/src/main/java/com/erp/service/CollectionService.java, backend-spring/src/main/java/com/erp/service/DataService.java, backend-spring/src/main/java/com/erp/service/DataAdvancedService.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    task-7과 동일한 변환 규칙 적용.
    ★ JdbcTemplate 사용 (유지 필요):
    - DataAdvancedService, PopProductionService, ShippingPlanService
    ★ Mapper 없는 Service (Dynamic SQL, BaseService 상속은 하되 sqlSession 사용):
    - DataService: 동적 SQL 구성 → sqlSession + JdbcTemplate 혼용 유지
    - MapDataService: ExternalDbConnectionService 주입 유지
    - PopActionService: NumberingRuleService 주입 유지
    ★ 여러 Mapper/Service 주입:
    - MultiConnectionService: ExternalDbConnectionMapper + ExternalDbConnectionService
      → Mapper를 sqlSession으로 변환, Service 주입은 유지
    - ReportService: CommonService + ExternalDbConnectionService 주입 유지
    - DigitalTwinService: CommonService + ExternalDbConnectionService 주입 유지

# ============================================================
# Phase 4: Mapper Interface 삭제 + XML namespace 변경 (task-17 ~ task-18)
# ============================================================

### task-17: Mapper Interface 96개 삭제 [backend]
- depends: task-7, task-8, task-9, task-10, task-11, task-12, task-13, task-14, task-15, task-16
- done_when: src/main/java/com/erp/mapper/ 디렉토리가 비어있거나 삭제됨
- files: backend-spring/src/main/java/com/erp/mapper/
- test: cd backend-spring && test $(find src/main/java/com/erp/mapper -name "*.java" 2>/dev/null | wc -l) -eq 0
- context: |
    src/main/java/com/erp/mapper/ 안의 모든 .java 파일 삭제.
    이 파일들은 더 이상 사용되지 않음 (Service가 sqlSession 직접 호출로 변경됨).
    디렉토리 자체는 남겨도 됨.

### task-18: XML namespace + 크로스 레퍼런스 일괄 변경 [backend]
- depends: task-7, task-8, task-9, task-10, task-11, task-12, task-13, task-14, task-15, task-16
- done_when: 모든 XML namespace가 소문자 + 모든 include refid가 새 namespace로 변경
- files: backend-spring/src/main/resources/mapper/*.xml
- test: cd backend-spring && grep -c 'namespace="com.erp' src/main/resources/mapper/*.xml | grep -v ':0$' | wc -l | xargs test 0 -eq
- verify: cd backend-spring && grep -c 'refid="com.erp' src/main/resources/mapper/*.xml | grep -v ':0$' | wc -l | xargs test 0 -eq
- context: |
    2가지를 동시에 변경해야 함:

    1) namespace 변경:
    변경 전: <mapper namespace="com.erp.mapper.AdminMapper">
    변경 후: <mapper namespace="admin">

    규칙: PascalCase에서 "Mapper" 제거 후 camelCase.
    AdminMapper → admin, TodoMapper → todo, DataAdvancedMapper → dataAdvanced
    CommonMapper → common (가장 중요!)

    2) include refid 크로스 레퍼런스 변경 (55개 파일에서 사용):
    변경 전: <include refid="com.erp.mapper.CommonMapper.pagination"/>
    변경 후: <include refid="common.pagination"/>

    변경 전: <include refid="com.erp.mapper.CommonMapper.companyCodeFilter"/>
    변경 후: <include refid="common.companyCodeFilter"/>

    변경 전: <include refid="com.erp.mapper.CommonMapper.dynamicOrderBy"/>
    변경 후: <include refid="common.dynamicOrderBy"/>

    변경 전: <include refid="com.erp.mapper.CommonMapper.notDeleted"/>
    변경 후: <include refid="common.notDeleted"/>

    변경 전: <include refid="com.erp.mapper.CommonMapper.auditInsertColumns"/>
    변경 후: <include refid="common.auditInsertColumns"/>

    변경 전: <include refid="com.erp.mapper.CommonMapper.auditUpdateSet"/>
    변경 후: <include refid="common.auditUpdateSet"/>

    ★ 반드시 모든 XML에서 "com.erp.mapper." 문자열이 0건인지 검증할 것.
    grep -r "com.erp.mapper" src/main/resources/mapper/ 결과가 0이어야 함.

    3) SQL 쿼리 포맷을 덕일 스타일로 통일 (96개 XML 전부):

    ★ 키워드/컬럼/테이블/별칭 전부 대문자:
    select → SELECT, from → FROM, where → WHERE, and → AND,
    left join → LEFT JOIN, order by → ORDER BY, group by → GROUP BY,
    insert into → INSERT INTO, update → UPDATE, delete → DELETE,
    as → AS, on → ON, in → IN, not → NOT, null → NULL,
    is → IS, like → LIKE, between → BETWEEN, exists → EXISTS,
    case → CASE, when → WHEN, then → THEN, else → ELSE, end → END,
    union all → UNION ALL, with recursive → WITH RECURSIVE,
    cast → CAST, coalesce → COALESCE, count → COUNT, sum → SUM,
    컬럼명: objid → OBJID, company_code → COMPANY_CODE, menu_type → MENU_TYPE 등
    별칭: v → V, s → S, cm → CM, menu → MENU 등
    테이블명: menu_info → MENU_INFO, company_mng → COMPANY_MNG 등

    ★ SELECT 컬럼은 한 줄에 하나씩, 쉼표는 컬럼 앞에:
    변경 전:
    SELECT v.lev, v.objid, v.parent_obj_id, v.menu_name_kor FROM v_menu v

    변경 후:
    SELECT
        V.LEV
        , V.OBJID
        , V.PARENT_OBJ_ID
        , V.MENU_NAME_KOR
    FROM V_MENU V

    ★ INSERT VALUES도 한 줄에 하나씩:
    변경 전:
    INSERT INTO MENU_INFO (objid, menu_name_kor, seq) VALUES (#{objid}, #{menuNameKor}, #{seq})

    변경 후:
    INSERT INTO MENU_INFO (
        OBJID
        , MENU_NAME_KOR
        , SEQ
    ) VALUES (
        #{objid}
        , #{menuNameKor}
        , #{seq}
    )

    ★ UPDATE SET도 한 줄에 하나씩:
    변경 전:
    UPDATE MENU_INFO SET menu_name_kor = #{menuNameKor}, seq = #{seq} WHERE objid = #{objid}

    변경 후:
    UPDATE MENU_INFO
    SET
        MENU_NAME_KOR = #{menuNameKor}
        , SEQ = #{seq}
    WHERE OBJID = #{objid}

    ★ JOIN, WHERE, ORDER BY 등 절 단위 줄 구분:
    변경 전:
    FROM v_menu v LEFT JOIN COMPANY_MNG cm ON v.company_code = cm.company_code WHERE v.status = 'active' ORDER BY v.seq

    변경 후:
    FROM V_MENU V
    LEFT JOIN COMPANY_MNG CM
        ON V.COMPANY_CODE = CM.COMPANY_CODE
    WHERE V.STATUS = 'active'
    ORDER BY V.SEQ

    ★ MyBatis #{파라미터}는 소문자 camelCase 유지 (Java 변수이므로):
    #{companyCode}, #{menuType}, #{userId} — 이건 대문자로 바꾸면 안 됨

    ★ MyBatis <if>, <choose>, <when> 태그 안의 test 조건도 camelCase 유지:
    <if test='companyCode != null'> — Java 변수명이므로 소문자 유지

    ★ SQL 함수는 대문자:
    now() → NOW(), upper() → UPPER(), to_char() → TO_CHAR(),
    array[] → ARRAY[], lpad() → LPAD()

# ============================================================
# Phase 5: XML 쿼리 로직 수정 (task-19)
# ============================================================

### task-19: company_code 필터 수정 — 공통 메뉴 포함 [backend]
- depends: task-18
- done_when: 모든 XML에서 companyCode 필터에 OR company_code = '*' 포함
- files: backend-spring/src/main/resources/mapper/*.xml
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- ref_files: backend-node/src/services/adminService.ts
- context: |
    모든 Mapper XML에서 company_code 필터링 시 공통 메뉴(company_code = '*')를 포함하도록 수정.

    변경 전:
    <if test='companyCode != null and companyCode != "*"'>
      AND company_code = #{companyCode}
    </if>

    변경 후:
    <if test='companyCode != null and companyCode != "*"'>
      AND (company_code = #{companyCode} OR company_code = '*')
    </if>

    단, SQL 본문에서 의도적으로 company_code = '*' 를 제외하는 쿼리는 건드리지 말 것
    (예: 사용자 목록에서 공통 계정 제외 등).
    Node 원본 서비스의 companyFilter 로직을 반드시 참고할 것.

# ============================================================
# Phase 6: 프론트엔드 || → ?? 수정 (task-20 ~ task-22)
# ============================================================

### task-20: MenuContext + AppLayout || → ?? 수정 [frontend]
- depends: none
- done_when: 메뉴 관련 || 연산자가 ?? 로 변경
- files: frontend/contexts/MenuContext.tsx, frontend/components/layout/AppLayout.tsx
- test: cd frontend && npx tsc --noEmit 2>&1 | tail -20
- context: |
    API 응답 필드에 || (OR) 연산자를 사용하면 숫자 0이 falsy로 처리되어 버그 발생.
    ?? (nullish coalescing) 으로 변경하면 null/undefined만 통과.

    변경 대상 패턴:
    item.OBJID || item.objid → item.OBJID ?? item.objid
    item.PARENT_OBJ_ID || item.parent_obj_id → item.PARENT_OBJ_ID ?? item.parent_obj_id
    item.SEQ || item.seq → item.SEQ ?? item.seq
    item.LEV || item.lev → item.LEV ?? item.lev
    menu.status || menu.STATUS → menu.status ?? menu.STATUS
    menu.seq || menu.SEQ || 0 → menu.seq ?? menu.SEQ ?? 0

    주의: 빈 문자열 ""을 falsy로 처리해야 하는 경우는 || 유지.
    예: userName || "Unknown" → 빈 문자열이면 "Unknown" 써야 하므로 || 유지.
    숫자 필드(objid, seq, lev, parent_obj_id 등)만 ?? 로 변경할 것.

### task-21: admin 컴포넌트 || → ?? 수정 [frontend]
- depends: none
- done_when: admin 관련 || 연산자가 ?? 로 변경
- files: frontend/components/admin/MenuFormModal.tsx, frontend/components/admin/MenuTable.tsx, frontend/components/admin/ScreenAssignmentTab.tsx, frontend/components/admin/MenuPermissionsTable.tsx, frontend/components/admin/dashboard/MenuAssignmentModal.tsx
- test: cd frontend && npx tsc --noEmit 2>&1 | tail -20
- context: |
    task-21과 동일한 규칙 적용.
    숫자 가능 필드 (objid, seq, lev, parent_obj_id, menu_type)에서 || → ?? 변경.

### task-22: screen/report/기타 컴포넌트 || → ?? 수정 [frontend]
- depends: none
- done_when: 나머지 컴포넌트의 || 연산자가 ?? 로 변경
- files: frontend/components/screen/MenuAssignmentModal.tsx, frontend/components/report/designer/MenuSelectModal.tsx, frontend/components/v2/config-panels/V2InputConfigPanel.tsx, frontend/app/(main)/admin/screenMng/dashboardList/[id]/page.tsx, frontend/app/(main)/admin/menu/page.tsx
- test: cd frontend && npx tsc --noEmit 2>&1 | tail -20
- context: |
    task-21과 동일한 규칙 적용.

# ============================================================
# Phase 7: 에러 처리 + 통합 빌드 (task-23 ~ task-24)
# ============================================================

### task-23: GlobalExceptionHandler 에러 응답 Node 호환 [backend]
- depends: task-2
- done_when: 에러 응답에 path 필드 포함
- files: backend-spring/src/main/java/com/erp/config/GlobalExceptionHandler.java
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- context: |
    Node 에러 응답: {"success": false, "message": "...", "path": "/api/xxx"}
    Spring 현재:   {"success": false, "message": "서버 내부 오류가 발생했습니다."}

    GlobalExceptionHandler에서:
    1. HttpServletRequest를 파라미터로 받아 request.getRequestURI()로 path 추가
    2. 500 에러 시 실제 에러 메시지 대신 제네릭 메시지 유지 (보안)
    3. 400, 404 에러 시 구체적 메시지 포함
    4. ApiResponse에 path 필드 추가 또는 Map으로 반환

### task-24: 전체 빌드 + 부트 검증 [backend]
- depends: task-6, task-17, task-18, task-19, task-23
- done_when: bootJar 성공
- files: backend-spring/build.gradle
- test: cd backend-spring && ./gradlew compileJava 2>&1 | tail -20
- verify: cd backend-spring && ./gradlew bootJar 2>&1 | tail -20
- context: |
    전체 빌드가 통과하는지 확인.
    Mapper Interface 삭제 후 import 에러가 없는지 확인.
    남은 컴파일 에러가 있으면 수정.
    MyBatis mapper-locations 설정이 여전히 classpath:mapper/*.xml 인지 확인.
    Mapper Interface 스캔 설정이 있으면 제거 (@MapperScan 등).
