Files
invyone/_pipeline_backup/runs/2026-03-27_pipe-20260327131904-jedw/plan.md
T

33 KiB

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 속성에서 작은따옴표 문자열 비교를 큰따옴표로 변경. 비어있지 않은 문자열 비교만 대상 (!= '' 은 제외).

    변경 전: 변경 후:

    변경 전: 변경 후:

    대상: 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 스타일):

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 변경: 변경 전: 변경 후:

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

    1. include refid 크로스 레퍼런스 변경 (55개 파일에서 사용): 변경 전: 변경 후:

    변경 전: 변경 후:

    변경 전: 변경 후:

    변경 전: 변경 후:

    변경 전: 변경 후:

    변경 전: 변경 후:

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

    1. 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 , , 태그 안의 test 조건도 camelCase 유지: — 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 = '*')를 포함하도록 수정.

    변경 전: AND company_code = #{companyCode}

    변경 후: AND (company_code = #{companyCode} OR company_code = '*')

    단, 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 등).