최초 작성일 | 2025.04.20 |
변경 작성일 | 2025.04.27 |
버전 | 1.1 |
프로그램 스펙서 ver 1.1 | 25.04.24
[조회 기본 조건 설정]
두 개의 조회 탭(MRP 조회, 생산오더 조회) 모두 사용자 편의를 위해 기본적으로 오늘 날짜 기준 3개월 이전부터 오늘까지의 데이터가 조회되도록 설정하였다. 이를 통해 최근 데이터를 빠르게 확인할 수 있도록 UX를 개선하였다.
[MRP 조회 조건]
MRP 조회 탭에서는 MRP ID 또는 계획일자 중 하나의 값만 입력해도 조회가 가능하도록 구현하였다. 사용자는 두 조건 중 편리한 항목을 선택하여 간편하게 검색할 수 있다.
[생산오더 조회 조건]
생산오더 조회 탭에서도 생산오더 ID 또는 계획 시작일 중 하나의 조건만 입력해도 조회가 가능하다. 동일하게 사용자의 검색 편의성을 고려한 설계이다.
[MRP 상태에 따른 생산오더 생성 가능 여부 체크]
MRP 조회 탭에서는 해당 MRP의 상태가 ‘생산오더 생성 가능’ 상태인지 여부를 판단하여 생성 가능 여부를 체크한다. 이를 통해 유효하지 않은 상태의 MRP로부터 생산오더가 생성되는 것을 방지하였다.
[생산오더 생성 시 화면 이동 및 데이터 연동]
사용자가 MRP로부터 생산오더를 생성하는 순간, 화면이 자동으로 생산오더 조회 탭으로 전환되며, 생성된 MRP와 생산오더 정보가 각각의 ALV에 실시간으로 출력된다. 이를 통해 흐름을 자연스럽게 이어가며 데이터를 확인할 수 있다.
[BOM ITEM 상세 정보 조회 기능]
하단의 생산오더 ALV에서 BOM ITEM 핫스팟을 클릭하면, 해당 자재에 대한 BOM ITEM 상세 정보가 조회된다. 이는 자재 구성 정보를 직관적으로 확인할 수 있는 기능이다.
[입고 배치 ID 자동 설정 (선입선출 기준)]
생산오더를 생성할 때, 사용되는 원유 및 원자재에 대해 재고 현황 테이블을 조회하여, 선입선출 기준으로 입고 배치 ID를 맵핑해서 생산 오더 아이템을 생성 해준다. 이 과정에서 생산량만큼의 가용재고가 차감되어 생성된다.
[생산계획 ID 연동 데이터 표시]
생산오더 조회 시, 해당 생산오더의 생산계획 ID를 기준으로 상단 영역에 관련된 MRP 데이터를 함께 출력한다. 이를 통해 생산계획과 생산오더 간의 연관성을 한눈에 파악할 수 있다.
[MRP ID 핫스팟 연동 기능]
생산오더로 생성된 MRP ID는 핫스팟으로 표시되며, 클릭 시 하단 영역에서 해당 생산계획 ID에 매핑된 생산오더 데이터로 화면이 자동 전환 또는 갱신된다. 이를 통해 양방향 조회가 가능하도록 UX를 향상시켰다.
전체구성 ver 1.0 | 25.04.20
생산 계획 조회
생산 오더 생성
생산 오더 조회
탭 전환 시 변경 되는 부분
개발 과정
•
모듈풀 프로그램
•
소요 일자 : 04/18 ~ 04.20 (3일)
•
추가 작업 : 04/25 ~ 04/27(2일)
설계 단계
우리 조는 다른 팀들과 달리 프로젝트 초기부터 전체 시스템 설계에 팀원 모두가 함께 참여했다.
모듈 테이블 설계 단계에서는 시간 효율을 위해 각자 담당 모듈을 나눠서 작업했는데, 나는 MM 모듈을 맡았다. 이후 각자의 설계를 공유하며 전체 구조를 다시 정리했고, 그 이후부터는 비즈니스 프로세스를 함께 구상하면서 본격적인 개발을 진행했다.
개발 단계에서는 모듈에 상관없이 기능 중심으로 섞어서 작업을 시작했는데, 내가 맡은 프로그램 대부분은 전체 프로세스에서 중후반부에 해당하는 기능들이었다. 그 중에서도 가장 먼저 개발을 시작한 건 생산 오더 관리 화면이었다. 이 화면은 전체 흐름상으로는 ‘생산 계획 → MRP 수립 → 생산 오더’의 마지막 단계에 해당하는데, 아이러니하게도 가장 먼저 착수하게 된 셈이다.
물론 이후에 MRP 담당자가 개발을 시작하면 테이블 구조나 필드명이 바뀔 수도 있다는 점을 염두에 두고, 전체 틀이 흔들리지 않도록 유연한 구조로 개발을 시작했다. 어차피 프로젝트 특성상 한두 번의 변경은 피할 수 없는 부분이라 생각했다.
화면 설계에서도 많은 고민이 있었다. 처음에는 ‘생산 계획’과 ‘생산 오더’를 하나의 ALV에서 탭으로 전환하는 방식도 고려했지만, 사용자 입장에서 한눈에 두 데이터를 비교할 수 있는 구성이 더 낫겠다고 판단했다. 그래서 ALV를 상단과 하단으로 분리하여 두 데이터를 동시에 보여주는 형태로 UI를 구성했다.
개발 진행
모듈풀에서 ALV는 처음인데 이참에 도전해봤다.
생산계획 조회할 때
FORM get_plan_data .
DATA: lv_mrpid TYPE zdat_pp140-mrpid.
" 유효성 검사
lv_mrpid = zdat_pp140-mrpid.
" MRPID 또는 MRP 수립일자(시작 또는 종료) 중 하나는 입력되어야 함
IF zdat_pp140-mrpid IS INITIAL AND lv_dat00_from IS INITIAL AND lv_dat00_to IS INITIAL.
MESSAGE 'MRPID 또는 수립일자 입력하세요.' TYPE 'I'.
RETURN.
ENDIF.
" 데이터 초기화
CLEAR: gt_plan, gs_plan.
" 생산계획 데이터 조회
SELECT *
FROM zdat_pp140
WHERE ( mrpid = @lv_mrpid OR @lv_mrpid IS INITIAL )
AND ( dat00 >= @lv_dat00_from OR @lv_dat00_from IS INITIAL )
AND ( dat00 <= @lv_dat00_to OR @lv_dat00_to IS INITIAL )
INTO CORRESPONDING FIELDS OF TABLE @gt_plan.
" 조회 결과 메시지
IF sy-subrc <> 0.
MESSAGE '조회 조건에 맞는 생산계획이 없습니다.' TYPE 'I'.
RETURN.
ENDIF.
PERFORM make_plan_data.
SORT gt_plan BY plnum DESCENDING.
ENDFORM.
ABAP
복사
•
날짜로 전체 조회
•
ID로 조회
•
ID 또는 날짜 둘 다 넣을 경우 둘 다 해당하는 데이터로 조회
이 쿼리는 실제 실행 시에도 정상적으로 작동하며, 개발 편의성 측면에서는 빠르게 조건 필터링을 구현할 수 있는 장점이 있다.
다만, 최근 SAP 교육 과정에서 다뤘던 표준 SELECT 구문 스타일이나 가독성, 유지보수 측면에서 생각했을 때는 WHERE절의 조건 표현 방식이 직관적이지 않다고 판단되었다.
그래서 Range 방식으로 바꿔서 진행했는데!
•
OR, IS INITIAL 조건들이 많아지면 가독성이 떨어지고 조건 해석이 어려움.
•
RANGE는 SAP 내 표준 SELECT 패턴으로, 조건 유무에 따라 동적으로 WHERE절을 구성할 수 있음
•
IN <range_table> 구조로 바꾸면 명확하게 '입력된 조건만 필터링' 하도록 구성 가능함
DATA: lv_mrpid TYPE zdat_pp140-mrpid.
TYPES: ty_range_mrpid TYPE RANGE OF zdat_pp140-mrpid,
ty_range_date TYPE RANGE OF zdat_pp140-dat00.
DATA: lt_mrpid_range TYPE ty_range_mrpid,
lt_date_range TYPE ty_range_date,
ls_mrpid_range LIKE LINE OF lt_mrpid_range,
ls_date_range LIKE LINE OF lt_date_range.
" 조건 초기화
CLEAR: gt_plan, gs_plan, lt_mrpid_range, lt_date_range.
" MRPID 조건
IF zdat_pp140-mrpid IS NOT INITIAL.
ls_mrpid_range-sign = 'I'.
ls_mrpid_range-option = 'EQ'.
ls_mrpid_range-low = zdat_pp140-mrpid.
APPEND ls_mrpid_range TO lt_mrpid_range.
ENDIF.
" 날짜 조건 (시작일)
IF lv_dat00_from IS NOT INITIAL.
ls_date_range-sign = 'I'.
ls_date_range-option = 'GE'.
ls_date_range-low = lv_dat00_from.
APPEND ls_date_range TO lt_date_range.
ENDIF.
" 날짜 조건 (종료일)
IF lv_dat00_to IS NOT INITIAL.
CLEAR ls_date_range.
ls_date_range-sign = 'I'.
ls_date_range-option = 'LE'.
ls_date_range-low = lv_dat00_to.
APPEND ls_date_range TO lt_date_range.
ENDIF.
" 유효성 검사
IF lt_mrpid_range IS INITIAL AND lt_date_range IS INITIAL.
MESSAGE 'MRPID 또는 수립일자 입력하세요.' TYPE 'I'.
RETURN.
ENDIF.
" 데이터 조회
SELECT *
INTO CORRESPONDING FIELDS OF TABLE gt_plan
FROM zdat_pp140
WHERE mrpid IN lt_mrpid_range
AND dat00 IN lt_date_range.
IF sy-subrc <> 0.
MESSAGE '조회 조건에 맞는 생산계획이 없습니다.' TYPE 'I'.
RETURN.
ENDIF.
PERFORM make_plan_data.
SORT gt_plan BY plnum DESCENDING.
ABAP
복사
향후 다른 프로그램 조건 필터링에도 이 방식으로 통일할 계획이다.
상태값 추가 요청
MRP 테이블에서 갖고와서 진행하다보니 생산 오더 진행 가능한 상태값 필드가 필요해서 담당 테이블 설계자랑 개발자한테 설명해주고 추가하게 됐다.
개발 프로그램 통일성
개발을 진행하다 보니, 팀 내 모든 프로그램이 통일성 있게 구성돼야 한다는 공감대가 생겼고, 이와 관련된 규칙을 디스코드에 공유하고 정리해두었다.
매크로를 사용하다
ALV 필드 카탈로그를 정의할 때는 반복되는 코드를 줄이기 위해 매크로를 사용해봤다.
" 순서 정렬 및 각 필드 간의 간격 설정 -> ABAP 매크로로 처리해버리긔...
DEFINE add_fcat_plan.
CLEAR ls_fcat.
ls_fcat-fieldname = &1.
ls_fcat-coltext = &2.
ls_fcat-col_pos = &3.
ls_fcat-outputlen = &4.
ls_fcat-key = ' '.
ls_fcat-hotspot = &5.
APPEND ls_fcat TO gt_fcat_p.
END-OF-DEFINITION.
add_fcat_plan 'PLNUM' '생산계획ID' 2 8 ''.
add_fcat_plan 'MRPID' 'MRP ID' 3 12 ''.
add_fcat_plan 'MATNR' '자재코드' 4 10 ''.
add_fcat_plan 'MATNM' '자재명' 5 15 ''.
add_fcat_plan 'DAT00' 'MRP 수립일자' 6 10 ''.
add_fcat_plan 'STLNR' 'BOM ID' 7 12 ''.
add_fcat_plan 'MNG01' '생산소요량' 8 10 ''.
add_fcat_plan 'MNG02' '현재재고량' 9 10 ''.
add_fcat_plan 'MNG03' '필요수량' 10 10 ''.
add_fcat_plan 'MEINS' '단위' 11 5 ''.
add_fcat_plan 'WERKS' '플랜트ID' 12 12 ''.
add_fcat_plan 'WERNM' '플랜트명' 13 8 ''.
add_fcat_plan 'PERNR' '사원ID' 14 10 ''.
add_fcat_plan 'UNAME' '사원명' 15 15 ''.
ABAP
복사
처음에는 PERFORM으로 공통 처리를 따로 빼서 재사용할까 고민했는데, ABAP에 매크로 기능이 있다는 걸 알게 되어 적용해봤다. 사용해보니 반복되는 필드 설정 코드를 간결하게 작성할 수 있어서 꽤 편리했다.
그러다가 매크로를 써도 좋은건가 싶어서 찾아봤더니 짧은 반복 코드라면 이렇게 매크로로 정의하는 게 개발 속도도 빠르고 보기에도 깔끔하다는 장점이 있었고, 단점으로는 디버깅이 어렵고, 매크로 정의 위치와 호출 위치가 멀어지면 오히려 혼란을 줄 수 있기 때문에 공통적으로 자주 반복되는 패턴에서만 최소한으로 사용하는 게 좋겠다 라고 판단하게 됐다.
ALV 팝업 생성
또 하나,
생산 오더 ALV에서 BOM ID를 선택했을 때 관련 BOM ITEM을 보여주는 팝업 ALV를 만들고 싶었는데,
처음에는 SCREEN 140을 새로 만들고 CUSTOM CONTROL로 ALV를 띄우는 방식으로 접근했다.
그런데 개발 도중 REUSE_ALV_GRID_DISPLAY를 쓰면 START_COLUMN, END_COLUMN, END_COLUMN, START_LINE ,END_LINE만 지정해주면 알아서 팝업창 형태로 ALV를 띄워준다는 걸 알게 됐다.
*&---------------------------------------------------------------------*
*& Form show_bom_items_popup
*&---------------------------------------------------------------------*
*& text
*&---------------------------------------------------------------------*
*& --> LV_STLNR
*&---------------------------------------------------------------------*
FORM show_bom_items_popup USING p_stlnr TYPE zdat_pp030-stlnr.
DATA: lt_bom_item TYPE TABLE OF zdat_pp030,
lt_fcat TYPE slis_t_fieldcat_alv,
ls_fcat TYPE slis_fieldcat_alv.
CLEAR: gt_bom.
" 1. 데이터 조회
SELECT *
INTO TABLE lt_bom_item
FROM zdat_pp030
WHERE stlnr = p_stlnr.
IF sy-subrc <> 0.
MESSAGE '해당 BOM 구성품이 없습니다.' TYPE 'I'.
RETURN.
ENDIF.
" 2. 필드카탈로그 설정
DEFINE add_fcat_bom.
CLEAR ls_fcat.
ls_fcat-fieldname = &1.
ls_fcat-seltext_m = &2.
APPEND ls_fcat TO lt_fcat.
END-OF-DEFINITION.
add_fcat_bom 'STLNR' 'BOM ID'.
add_fcat_bom 'MATNR' '자재 코드'.
add_fcat_bom 'MATNM' '자재명'.
add_fcat_bom 'ZMENG' '수량'.
add_fcat_bom 'MEINS' '자재단위'.
" 3. ALV 뿌려
CALL FUNCTION 'REUSE_ALV_GRID_DISPLAY'
EXPORTING
* I_INTERFACE_CHECK = ' '
* I_BYPASSING_BUFFER = ' '
* I_BUFFER_ACTIVE = ' '
* I_CALLBACK_PROGRAM = ' '
* I_CALLBACK_PF_STATUS_SET = ' '
* I_CALLBACK_USER_COMMAND = ' '
* I_CALLBACK_TOP_OF_PAGE = ' '
* I_CALLBACK_HTML_TOP_OF_PAGE = ' '
* I_CALLBACK_HTML_END_OF_LIST = ' '
* i_structure_name = 'ZDAT_PP030'
* I_BACKGROUND_ID = ' '
i_grid_title = '생산오더 BOM Item'
* I_GRID_SETTINGS =
* IS_LAYOUT =
it_fieldcat = lt_fcat
* IT_EXCLUDING =
* IT_SPECIAL_GROUPS =
* IT_SORT =
* IT_FILTER =
* IS_SEL_HIDE =
* I_DEFAULT = 'X'
* I_SAVE = ' '
* IS_VARIANT =
* IT_EVENTS =
* IT_EVENT_EXIT =
* IS_PRINT =
* IS_REPREP_ID =
i_screen_start_column = 10
i_screen_start_line = 5
i_screen_end_column = 130
i_screen_end_line = 10
* I_HTML_HEIGHT_TOP = 0
* I_HTML_HEIGHT_END = 0
* IT_ALV_GRAPHICS =
* IT_HYPERLINK =
* IT_ADD_FIELDCAT =
* IT_EXCEPT_QINFO =
* IR_SALV_FULLSCREEN_ADAPTER =
* O_PREVIOUS_SRAL_HANDLER =
* O_COMMON_HUB =
* IMPORTING
* E_EXIT_CAUSED_BY_CALLER =
* ES_EXIT_CAUSED_BY_USER =
TABLES
t_outtab = lt_bom_item
EXCEPTIONS
program_error = 1
OTHERS = 2.
IF sy-subrc <> 0.
ENDIF.
" 스크린 140 필요없어짐..ㅋㅋ
* CALL SCREEN 140
* STARTING AT 10 5
* ENDING AT 120 25.
ENDFORM.
ABAP
복사
알고 나서는 기존 커스터마이징한 화면 코드는 주석 처리하고 바로 바꿨다. 덕분에 더 간단하고 깔끔한 구조로 개선할 수 있었다.
탭 분기 처리
하나의 화면에서 생산계획 ALV와 생산오더 ALV를 함께 보여주고 있었기 때문에,
이벤트 발생 시 어떤 ALV에서 발생했는지를 구분해서 처리할 필요가 있었다.
처음에는 각 메소드마다 IF sender = go_alv_grid_p ... 식으로 반복 작성했었는데,
중복되는 조건 처리를 줄이기 위해 get_grid_number라는 공통 메서드를 만들어 활용했다
" 선택한 ALV 그리드를 SENDER로 판단
CLASS-METHODS : get_grid_number
IMPORTING i_sender TYPE REF TO cl_gui_alv_grid
RETURNING VALUE(rv_grid_num) TYPE i.
ABAP
복사
" 선택한 ALV 그리드 판단
METHOD get_grid_number.
IF i_sender = go_alv_grid_p.
rv_grid_num = 1.
ELSEIF i_sender = go_alv_grid_o.
rv_grid_num = 2.
ENDIF.
ENDMETHOD.
ABAP
복사
활용 예시
" 사용자 명령 처리 로직
METHOD on_user_command.
DATA: lv_grid_num TYPE i.
lv_grid_num = get_grid_number( sender ).
PERFORM on_user_command USING e_ucomm gv_rowid lv_grid_num.
ENDMETHOD.
" 컨텍스트 메뉴 요청 처리 로직
METHOD on_context_menu_request.
DATA: lv_grid_num TYPE i.
lv_grid_num = get_grid_number( sender ).
PERFORM on_context_menu_request USING e_object lv_grid_num CHANGING gv_rowid.
ENDMETHOD.
" ALV BOM ITEM 핫스팟 클릭 메소드
METHOD on_handle_hotspot_click.
DATA: lv_grid_num TYPE i.
lv_grid_num = get_grid_number( sender ).
PERFORM on_handle_hotspot_click USING e_row_id e_column_id es_row_no lv_grid_num.
ENDMETHOD.
ABAP
복사
이렇게 구현하니까 이후 로직에서는 lv_grid_num = 1이면 생산계획, 2면 생산오더로
조건 분기를 명확하게 할 수 있어서 훨씬 깔끔해졌다.
사건의 발단 | 25.04.25
생산오더 생성 시 입고배치ID 매핑 처리 프로세스 변경 기록
1. 배경
생산공정 진행 단계에서 원유 입고배치ID가 꼭 필요하다는 요구사항이 발생했다.
생산공정 시, 사용되는 원유의 출처를 추적 관리해야 하며, 따라서 생산오더 생성 시점에 입고배치ID를 확보해놓을 필요가 생겼다.
이에 따라 MRP 개발자, 생산오더 개발자(나) 간 대면 회의가 진행되었다. + 생산계획 개발자도 같이 참여.
2. 문제 제기
처음에는 "입고배치ID를 누가 줄 것인가"에 대한 논의부터 시작했다.
•
MRP 테이블은 재고 요청만 발생하고, 실제 입고 및 가용 상태 관리까지는 하지 않기 때문에, MRP 테이블에서는 입고배치ID를 매핑할 수 없는 구조였다.
•
결과적으로 생산오더 생성 시점에서 입고배치ID를 매핑해야 한다는 결론에 도달했다.
3. 구조 변경
원래 생산오더 테이블은 헤더와 아이템이 1:1 매핑 구조로 설계되어 있었다.
하지만 입고배치ID를 관리하기 위해 헤더:아이템 = 1:다 구조로 수정해야 했다.
•
생산오더 테이블 설계 담당자에게 구조 변경 요청
•
테이블 구조 수정 및 프로그램 로직 재개발 진행
4. 상세 처리 로직
생산오더 생성 시 다음과 같은 흐름으로 입고배치ID를 매핑하도록 변경했다.
•
생산하려는 제품(예: 흰우유 200ML)에 필요한 원유 필요수량은 MRP 테이블에서 전달받는다.
•
재고현황 테이블을 조회하여 입고된 원유 입고배치ID를 생산 필요량만큼 매핑한다.
•
재고는 선입선출(FIFO) 방식으로 사용한다. (가장 먼저 입고된 재고부터 차감)
•
필요한 양을 채우기 위해 여러 입고배치를 연결해서 사용 가능하다.
•
원유뿐만 아니라, BOM상 연결된 다른 부자재 원자재들도 함께 매핑하여 생산오더 아이템에 추가한다.
•
생산오더 생성 시 재고현황 테이블의 가용 수량을 차감하여 일관성 있게 관리한다.
5. 추가 프로세스 점검
이 작업을 진행하면서 생산계획(MPS), MRP, 생산오더 프로세스 전반을 다시 점검했다.
•
판매계획(Sales Plan) → 판매오더(Sales Order) → 생산계획 → MRP → 생산오더 → 생산공정 흐름을 종합적으로 검토
•
기존에는 생산오더 생성 가능 여부를 단순히 MRP 상태로 판단했으나, 품질검사 통과 여부와 입고 완료 여부까지 고려해야 한다는 점을 반영했다.
특히, MRP 테이블의 상태값 처리 로직도 추가로 조정했다.
•
입고된 원자재의 품질검사가 완료되면
•
자재현황 테이블과 MRP 테이블을 기준으로
•
MRP 수립일자와 같은 날짜를 기준으로 해당 MRP 레코드의 상태값을 04(생산오더 생성 가능) 으로 업데이트한다.
6. 마무리 소감
처음에는 단순한 손풀기용 생산오더 개발로 시작했지만, 결과적으로 재고관리–생산계획–생산공정 흐름 전체를 다시 조율해야 하는 예상 밖의 난이도 상승을 경험했다.
이번 과정을 통해 SAP 프로젝트에서는 작은 요구사항 변경이 전반적인 프로세스 검토로 이어질 수 있다는 점을 몸소 체감할 수 있었다. 또한 테이블 구조와 데이터 흐름을 처음 설계할 때부터 충분히 고민하는 것이 얼마나 중요한지 다시 한번 깨달았다.
비록 테이블 구조 설계는 내가 직접 담당한 부분은 아니었지만, 구조 변경이 필요한 상황을 빠르게 파악하고 설계자와 긴밀히 협의하여 문제를 해결해 나가는 과정 역시 개발자의 중요한 역할임을 깊이 느낄 수 있었다.