14 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:
@ResponseBodywith 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_ENDparameters 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}::integerfor 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 sessionhasSession(session)— Checks if PERSON_BEAN existsdestroy(session)— Removes session attributes and invalidates
SqlMapConfig (src/com/pms/common/utils/SqlMapConfig.java)
- Singleton pattern:
SqlMapConfig.getInstance().getSqlSession() - Reads
mybatisConf.xmlfor DataSource and mapper configuration
Adding New Features
To add a new screen/feature, follow the existing pattern:
- Controller — Create
{Feature}Controller.javawith@Controller,@RequestMappingfor*.doURLs - Service — Create
{Feature}Service.javawith@Service, useSqlMapConfig.getInstance().getSqlSession() - Mapper XML — Create
{feature}.xmlwith namespace matching, register inmybatisConf.xml - JSP View — Create in
WebContent/WEB-INF/view/{module}/, start with<%@include file="/init.jsp" %> - Wire —
@Autowiredservice 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.