Files
wace_plm/CLAUDE.md
hjjeong 0ed40b91d5 .gitignore 업데이트 및 CLAUDE.md 메뉴구조/주의사항 추가
- .gitignore: CLAUDE.md, phoenix/, *.pgsql 추가
- CLAUDE.md: 메뉴 구조 테이블, Java 7 주의사항, 코딩 컨벤션 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:16:06 +09:00

16 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

WACE-PLM (웨이스 PLM) — a Java-based enterprise Resource Planning System (RPS) for managing business processes, resources, projects, and documents. Korean-language UI and documentation throughout.

Tech Stack

  • Backend: Java 7, Spring Framework 3.2.4, MyBatis 3.2.3
  • Frontend: JSP (1,100+ views), jQuery 2.1.4, Select2, SweetAlert2, jqGrid 4.7.1, Tabulator/TUI-Grid, Highcharts, dhtmlxGantt, SmartEditor 2 (HuskyEZCreator)
  • Database: PostgreSQL (JNDI datasource java:/comp/env/plm)
  • Server: Apache Tomcat 7.0.94
  • Build: Eclipse IDE-based (no Maven/Gradle), Docker for dev/prod
  • CSS: Custom basic.css, Bootstrap, jQuery UI, Select2 CSS, Tabulator CSS

Build & Run Commands

# Development environment (Docker)
docker-compose -f docker-compose.dev.yml up --build -d
docker-compose -f docker-compose.dev.yml down

# Compile only
./compile_only.sh

# Full rebuild + restart
./rebuild-and-restart.sh

# Production
./start-prod-full.sh

Dev port: 9090 (maps to 8080 in container). JVM: Xms=512m, Xmx=1024m.

Architecture

MVC Pattern (Spring + MyBatis)

All source code lives under src/com/pms/. The request flow is:

HTTP Request (*.do) → Controller (@RequestMapping) → Service (@Service) → SqlSession → Mapper XML → PostgreSQL
  • View resolution: prefix /WEB-INF/view, suffix .jsp
  • JSON responses: @ResponseBody with Jackson converter
  • Component scan: base package com.pms
  • Session timeout: 1440 minutes (24 hours)

Module Structure (src/com/pms/)

Package Domain Scale
salesmgmt/ Sales management (contracts, orders, estimates, delivery, dealers, goods, inspections) 25+ controllers, 20+ mappers
ions/itemmgmt/ Item/BOM management, purchasing, material warehousing 9 controllers, 8 mappers
ions/productioninventory/ Production & inventory controllers + mappers
controller/, service/, mapper/ Core modules (admin, login, specs, distribution, project, quality, dashboard) root-level
api/ External API integrations (ERP sync, Amaranth approval/user) 11 client classes
common/ Shared utilities, beans, base services CommonUtils, Constants, SessionManager

View Directory Structure (WebContent/WEB-INF/view/)

Major directories: admin/ (40+ sub-modules), salesmgmt/ (20+ sub-modules), ions/ (itemmgmt, productioninventory), project/ (gate, partMaster, wbs), board/, bom/, contractMgmt/, costMgmt/, dashboard/, documentMng/, fundMgmt/, inventoryMng/, orderMgmt/, part/, quality/, productionplanning/, purchaseOrder/, and more.

Key Configuration Files

File Purpose
WebContent/WEB-INF/web.xml Deployment descriptor (UTF-8 filter, *.do mapping, 1440min session, error pages)
WebContent/WEB-INF/dispatcher-servlet.xml Spring MVC config (component scan, view resolver, Jackson JSON, scheduler pool=10)
src/com/pms/mapper/mybatisConf.xml MyBatis config with 52 registered mapper XML files
WebContent/WEB-INF/log4j.xml Logging config (DEBUG level, daily rolling)
tomcat-conf/context.xml JNDI DataSource (max 200 connections, 50 idle, 10s wait)
.env.development Dev DB: jdbc:postgresql://211.115.91.141:11133/waceplm
.env.production Prod DB: Docker internal wace-plm-db:5432/waceplm

Database

PostgreSQL with MyBatis XML mappers (52 registered). Automated backup via db/backup.py (2x daily, 7-day retention). Database dump: db/dbexport.pgsql.

Table naming: UPPERCASE_SNAKE_CASE (primary: USER_INFO, PART_MGMT, ORDER_MGMT). Some legacy tables use ERP-style naming (SWSB110A_TBL). Column naming: UPPERCASE_SNAKE_CASE (USER_ID, PART_NO, DEL_YN). Boolean columns: DEL_YN with Y/N values.


Coding Conventions

Controller Pattern

@Controller
public class {Feature}Controller {
    @Autowired
    private {Feature}Service service;

    // List page
    @RequestMapping(value = "/{module}/{feature}List.do", method = RequestMethod.GET)
    public String get{Feature}All(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) {
        // ... returns JSP path string
    }

    // Form popup
    @RequestMapping(value = "/{module}/{feature}FormPopup.do", method = RequestMethod.GET)
    public String {feature}FormPopup(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) { ... }

    // Save (AJAX)
    @ResponseBody
    @RequestMapping(value = "/{module}/save{Feature}.do", method = RequestMethod.POST)
    public Map<String, Object> save{Feature}(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) { ... }

    // Delete (AJAX)
    @ResponseBody
    @RequestMapping(value = "/{module}/delete{Feature}.do", method = RequestMethod.POST)
    public Map<String, Object> delete{Feature}(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) { ... }
}

Naming: {Feature}Controller.java — CamelCase. Methods: get{Feature}All, save{Feature}, delete{Feature}.

Service Pattern

@Service
public class {Feature}Service {
    @Autowired
    CommonService commonService;

    public List<Map<String,Object>> get{Feature}All(HttpServletRequest request, Map<String, Object> paramMap) {
        SqlSession sqlSession = null;
        try {
            sqlSession = SqlMapConfig.getInstance().getSqlSession();

            // Pagination
            String countPerPage = CommonUtils.checkNull(request.getParameter("countPerPage"), Constants.ADMIN_COUNT_PER_PAGE+"");
            paramMap.put("COUNT_PER_PAGE", Integer.parseInt(countPerPage));
            Map pageMap = (HashMap) sqlSession.selectOne("namespace.get{Feature}ListCnt", paramMap);
            pageMap = (HashMap) CommonUtils.setPagingInfo(request, pageMap, countPerPage);
            paramMap.put("PAGE_END", CommonUtils.checkNull(pageMap.get("PAGE_END")));
            paramMap.put("PAGE_START", CommonUtils.checkNull(pageMap.get("PAGE_START")));

            resultList = (ArrayList) sqlSession.selectList("namespace.get{Feature}List", paramMap);
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            sqlSession.close();
        }
        return CommonUtils.toUpperCaseMapKey(resultList);
    }
}

Key points:

  • SqlSession obtained via SqlMapConfig.getInstance().getSqlSession() — must close in finally block.
  • Result maps converted to uppercase keys via CommonUtils.toUpperCaseMapKey().
  • Pagination uses PAGE_START/PAGE_END parameters with count query + list query.

Mapper XML Pattern

<mapper namespace="{module}.{feature}">
    <!-- Count for pagination -->
    <select id="get{Feature}ListCnt" parameterType="map" resultType="map">
        SELECT COUNT(*) AS TOTAL_COUNT FROM TABLE_NAME WHERE 1=1
        <if test='condField != null and condField != ""'>AND FIELD = #{condField}</if>
    </select>

    <!-- List with pagination -->
    <select id="get{Feature}List" parameterType="map" resultType="map">
        SELECT * FROM (
            SELECT ROW_NUMBER() OVER (ORDER BY CRET_DATE) AS RNUM, ...
            FROM TABLE_NAME WHERE 1=1
            <if test='condField != null and condField != ""'>AND FIELD = #{condField}</if>
        ) T WHERE 1=1
        <if test="PAGE_END != null and PAGE_END != ''">
            <![CDATA[ AND RNUM <= #{PAGE_END}::integer ]]>
        </if>
        <if test="PAGE_START != null and PAGE_START != ''">
            <![CDATA[ AND RNUM >= #{PAGE_START}::integer ]]>
        </if>
    </select>

    <!-- Single record -->
    <select id="get{Feature}" parameterType="map" resultType="map"> ... </select>

    <!-- Insert/Update -->
    <insert id="insert{Feature}" parameterType="map"> ... </insert>

    <!-- Delete -->
    <delete id="delete{Feature}" parameterType="map"> ... </delete>
</mapper>

Naming conventions:

  • File: {feature}.xml (camelCase)
  • Namespace: {module}.{feature} or just {feature} for root-level
  • SQL IDs: get{Feature}List, get{Feature}ListCnt, get{Feature}, insert{Feature}, delete{Feature}
  • Parameter type casting: #{param}::numeric, #{param}::integer for PostgreSQL
  • All parameterType="map", resultType="map" (no typed beans for queries)

JSP Pattern

Every JSP starts with:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.pms.common.utils.*"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ page import="java.util.*" %>
<%@include file="/init.jsp" %>

init.jsp (common include) provides:

  • Session check and PersonBean extraction (connectUserId, connectUserDeptCode, connectUserName, etc.)
  • All CSS/JS includes (jQuery, Select2, SweetAlert2, Tabulator, common.js, datepicker)
  • Loading overlay markup
  • Page authorization AJAX call

List page structure:

Search form (#plmSearchZon) → Button group (.plm_btn_wrap) → Data table (.plm_table) → Pagination (.pdm_page)

Form popup structure:

Hidden fields → Form table (#adminPopupForm) → Button group (저장/닫기)

No Tiles framework — layout is managed through init.jsp include and consistent HTML structure.

JavaScript/AJAX Patterns

Standard AJAX call:

$.ajax({
    url: "/module/action.do",
    type: "POST",
    data: {"param1": value1, "param2": value2},
    dataType: "json",
    success: function(data) {
        if (data == "SUCCESS") {
            Swal.fire("완료되었습니다.");
            fn_search();
        }
    },
    error: function(jqxhr, status, error) {
        Swal.fire(jqxhr.statusText + ", " + status + ", " + error);
    }
});

Popup pattern:

window.open("", "formPopup", "width=1150, height=676");
document.form1.action = "/module/formPopup.do";
document.form1.target = "formPopup";
document.form1.submit();

// From popup back to parent:
opener.fn_search();
self.close(0);

Key functions in common.js (WebContent/js/common.js, 3500+ lines):

Category Functions
Validation fnc_valitate(formName), fnc_validate(formName), fnc_checkDataType(dataType, value)
Null check fnc_isEmpty(val), fnc_isNotEmpty(val), fnc_checkNull(val), fnc_checkNullDefaultValue(val, default)
Date fnc_datepick() (auto-init on ID containing "Date"), fnc_monthpick(), fnc_getDate(delimiter), fnc_dateCheck(from, to)
Pagination fnc_goPrev(page), fnc_goNext(page), fnc_goPage(page), fnc_goStart(), fnc_goEnd(maxPage)
Code lookup fnc_getCodeList(codeId), fnc_getCodeJsonStr(codeId)
File ops fnc_deleteFile(fileObjId, callback), fnc_downloadFile(fileObjId)
Grid fnc_createGridAnchorTag(cell, params, onRendered)
Form serializeObject() (jQuery extension), custom Map class

Common AJAX endpoints: /common/getCodeList.do, /common/getSupplyCodeList.do, /common/getClientMngList.do, /common/getProjectNoList.do, /common/getDeptList.do, /common/searchUserList.do, /common/getCarTypeList.do, /common/getOEMList.do, /common/getPartMngList.do, /common/getProductMgmtList.do


Key Utility Classes

CommonUtils (src/com/pms/common/utils/CommonUtils.java)

Method Purpose
checkNull(Object obj) Returns empty string if null
isBlank(Object) / isNotBlank(Object) Null/empty check
isEmpty(Object) / isNotEmpty(Object) Null/empty check
setPagingInfo(request, pageMap, countPerPage) Calculate pagination
toUpperCaseMapKey(List/Map) Convert map keys to uppercase
createObjId() Generate unique object ID
Date format constants DATE_FORMAT1="yyyy-MM-dd", DATE_FORMAT2="yyyyMMdd", etc.

Constants (src/com/pms/common/utils/Constants.java)

Constant Value Purpose
PERSON_BEAN "PERSON_BEAN" Session key for logged-in user
COUNT_PER_PAGE 25 Default pagination size
ADMIN_COUNT_PER_PAGE 25 Admin pagination size
FILE_STORAGE "/data_storage" File storage root
AJAX_RESPONSOR "/common/ajaxResponsor" AJAX response view
SYSTEM_CHARSET "UTF-8" System encoding
SUPER_ADMIN "plm_admin" Super admin user ID

SessionManager (src/com/pms/common/utils/SessionManager.java)

  • setSessionManage(request, paramMap) — Creates PersonBean from login query, stores in session
  • hasSession(session) — Checks if PERSON_BEAN exists
  • destroy(session) — Removes session attributes and invalidates

SqlMapConfig (src/com/pms/common/utils/SqlMapConfig.java)

  • Singleton pattern: SqlMapConfig.getInstance().getSqlSession()
  • Reads mybatisConf.xml for DataSource and mapper configuration

Adding New Features

To add a new screen/feature, follow the existing pattern:

  1. Controller — Create {Feature}Controller.java with @Controller, @RequestMapping for *.do URLs
  2. Service — Create {Feature}Service.java with @Service, use SqlMapConfig.getInstance().getSqlSession()
  3. Mapper XML — Create {feature}.xml with namespace matching, register in mybatisConf.xml
  4. JSP View — Create in WebContent/WEB-INF/view/{module}/, start with <%@include file="/init.jsp" %>
  5. Wire@Autowired service into controller

Pagination: Use count query (get{Feature}ListCnt) + list query (get{Feature}List) with PAGE_START/PAGE_END pattern. Session: Get user via request.getSession().getAttribute(Constants.PERSON_BEAN)PersonBean. Grids: Use jqGrid or Tabulator depending on module. Sales modules increasingly use Tabulator. Alerts: Use Swal.fire() (SweetAlert2) for user notifications. Popups: Use window.open() with form target submission.

External Integrations

11 API client classes in src/com/pms/api/ handle ERP synchronization (parts, sales slips, warehouses), Amaranth system (approvals, users), and master data sync (accounts, customers, departments, employees).

No Automated Tests

There is no test framework configured. Testing is manual via browser.


메뉴 구조

대메뉴 주요 URL
영업관리 상담, 견적(estimateList_new), 주문서(orderMgmtList), 판매, 매출
프로젝트관리 WBS관리, 진행관리, 손익산출
개발관리 PART 등록/조회, E-BOM 등록/조회, 설계변경
구매관리 M-BOM, 구매리스트, 견적요청서, 품의서, 발주서, 입고
구매요청 구매요청서, 품의서
자재관리 자재리스트, 불출의뢰서, 자금관리
생산관리 M-BOM, 생산계획&실적, 반제품/원자재 소요량
품질관리 수입검사, 공정검사, 반제품검사
고객CS관리 고객CS
ECR관리 ECR

주의사항

  • Java 7 프로젝트이므로 람다, 스트림 등 Java 8+ 문법 사용 불가
  • 소스(src/)와 컴파일 결과(WebContent/WEB-INF/classes/)에 동일한 매퍼 XML이 존재하지만 컴파일하면 자동 동기화되므로 소스만 수정
  • SQL은 MyBatis XML에 직접 작성하며, 어노테이션 기반 매퍼는 사용하지 않음
  • 커밋 메시지는 한국어로 작성 (변경 내용 설명)
  • 다국어 지원: 한국어(ko), 영어(en), 일본어(jp) - MessageUtils 사용