# (주)삼안 ERP 시스템 (Satis) 초상세 로우레벨 분석 명세서

본 문서는 (주)삼안의 **ERP 시스템 (`Satis`)**의 실제 소스 코드를 직접 분석한  
**초상세 로우레벨(Ultra-Detailed Low-Level) 구현 명세서**입니다.  
`OracleClass.php` 전체 메소드 명세, **비즈니스 로직(Model / `Satis/sys/model/*`) 20개 모듈 구조**, 전자결재 알고리즘,  
문서번호 채번 방식, 인코딩 파이프라인, 결재 서명 렌더링, 시스템 글로벌 변수, DB 데이터 모델, 보안 핸들링 등을 명세합니다.

---

## 1. 모듈별 컨트롤러, 모델 & 뷰 맵핑 딕셔너리 (Controller - Model - View Map)

### 1.1 회계 관리 모듈 (`Account`) — 30개 컨트롤러
| 컨트롤러 (`Satis/sys/controller/Account/`) | 대응 View (`Smarty/templates/Satis/Account/`) | 주요 기능 |
| :--- | :--- | :--- |
| `AccountSlipTemp_Controller.php` | `AccountSlipTemp.tpl` | 임시전표 작성/조회/수정/결재 상신 |
| `AccountSlipConfirmed_Controller.php` | `AccountSlipConfirmed.tpl` | 확정전표 조회 및 결재 상태 확인 |
| `AccountSlipAutoManager_Controller.php` | `AccountSlipAutoManager.tpl` | 더존 iCube ERP 자동전표 전송 |
| `AccountBalanceStatement_Controller.php` | `AccountBalanceStatement.tpl` | 합계잔액시산표 생성 |
| `AccountFinancialStatement_Controller.php` | `AccountFinancialStatement.tpl` | 손익계산서(P/L) / 대차대조표(B/S) |
| `AccountGisungCalculation_Controller.php` | `AccountGisungCalculation.tpl` | 프로젝트 기성 청구금액 산출 |
| `AccountJointOfficeBudgetManager_Controller.php` | `AccountJointOfficeBudget.tpl` | 공동도급(JV) 예산/지분율 원가 관리 |
| `AccountFixAsset_Controller.php` | `AccountFixAsset.tpl` | 고정자산 감가상각비/대장 관리 |
| `AccountCorporateTax_Controller.php` | `AccountCorporateTax.tpl` | 법인세 계산 및 세무 조정 |
| `AccountWithHoldingTax_Controller.php` | `AccountWithHoldingTax.tpl` | 원천세 신고 / 원천징수 영수증 |
| `AccountVAT_Controller.php` | `AccountVAT.tpl` | 부가가치세 신고 데이터 집계 |
| `AccountCostCalculation_Controller.php` | `AccountCostCalculation.tpl` | 원가 계산 및 분석 |
| `AccountCostManagement_Controller.php` | `AccountCostManagement.tpl` | 원가 관리 현황 |
| `AccountFundPlan_Controller.php` | `AccountFundPlan.tpl` | 자금 계획 관리 |
| `AccountDepositsManager_Controller.php` | `AccountDepositsManager.tpl` | 예금/차입금 관리 |
| `AccountLoanManager_Controller.php` | `AccountLoanManager.tpl` | 대출 관리 |
| `AccountSecurities_Controller.php` | `AccountSecurities.tpl` | 유가증권 관리 |
| `AccountEntertainmentExpenses_Controller.php` | `AccountEntertainmentExpenses.tpl` | 접대비 관리 |
| `AccountCarManager_Controller.php` | `AccountCarManager.tpl` | 차량 운행/유지비 관리 |
| `AccountDailyWorkTask_Controller.php` | `AccountDailyWorkTask.tpl` | 일일 업무 실적 처리 |
| `AccountSummary_Controller.php` | `AccountSummary.tpl` | 회계 요약 현황 |
| `AccountSummaryReport_Controller.php` | `AccountSummaryReport.tpl` | 회계 종합 보고서 |
| `AccountDeadlineManager_Controller.php` | `AccountDeadlineManager.tpl` | 결산 기한 관리 |
| `AccountMonthSupplementary_Controller.php` | `AccountMonthSupplementary.tpl` | 월별 추가 분개 관리 |
| `AccountBillCheckManager_Controller.php` | `AccountBillCheckManager.tpl` | 어음/수표 관리 |
| `AccountFinaCode_Controller.php` | `AccountFinaCode.tpl` | 재무 코드 관리 |
| `AccountFundCode_Controller.php` | `AccountFundCode.tpl` | 자금 코드 관리 |
| `AccountAssetCode_Controller.php` | `AccountAssetCode.tpl` | 자산 코드 관리 |
| `FinalClosing_Controller.php` | `FinalClosing.tpl` | 결산 확정 처리 |
| `AccountGisungCalculation_Controller.php` | `AccountGisungCalculation.tpl` | 기성 계산 처리 |

### 1.2 인사/급여 관리 모듈 (`Person`) — 23개 컨트롤러
| 컨트롤러 (`Satis/sys/controller/Person/`) | 대응 View (`Smarty/templates/Satis/Person/`) | 주요 기능 |
| :--- | :--- | :--- |
| `PersonInfoManager_Controller.php` | `PersonInfoManager.tpl` | 인사기록 카드, 신상정보, 이력 관리 |
| `PersonAppointment_Controller.php` | `PersonAppointment.tpl` | 발령 처리 (입사/퇴사/이동/승진/전보) |
| `PersonInsaLicense_Controller.php` | `PersonInsaLicense.tpl` | 엔지니어링 기술자 자격증 관리 |
| `PersonCode_Controller.php` | `PersonCode.tpl` | 인사 코드 마스터 관리 |
| `PersonTime_Controller.php` | `PersonTime.tpl` | 근태 관리 (연차/야근/출퇴근) |
| `PersonPayBase_Controller.php` | `PersonPayBase.tpl` | 급여 기준 정보 설정 |
| `PersonPayBasic_Controller.php` | `PersonPayBasic.tpl` | 기본급/수당 항목 설정 |
| `PersonPaymentCalculation_Controller.php` | `PersonPaymentCalculation.tpl` | 월 급여/상여 계산 엔진 |
| `PersonPaymentReport_Controller.php` | `PersonPaymentReport.tpl` | 급여명세서 / 부서별 지급 총괄 |
| `PersonSlipPaymentManager_Controller.php` | `PersonSlipPaymentManager.tpl` | 급여 전표 연동 처리 |
| `PersonRetirementPay_Controller.php` | `PersonRetirementPay.tpl` | 퇴직금 계산 및 충당금 정산 |
| `PersonYearEndTax_Controller.php` | `PersonYearEndTax.tpl` | 연말정산 (`yeta_data` 연동) |
| `PersonInsaReport_Controller.php` | `PersonInsaReport.tpl` | 인사 현황 보고서 |
| `PersonInsaStatusReport_Controller.php` | `PersonInsaStatusReport.tpl` | 인사 상태 보고서 |
| `PersonOrganiz_Controller.php` | `PersonOrganiz.tpl` | 조직도 관리 |
| `Duzon_link_controller.php` | `Duzon_link.tpl` | 더존 인사/급여 ERP 데이터 싱크 |
| `auth_pay_controller.php` | `auth_pay.tpl` | 급여 지급 권한 처리 |
| `emp_check_controller.php` | `emp_check.tpl` | 재직 여부 확인 |

### 1.3 프로젝트/현장 공정 모듈 (`Project`) — 15개 컨트롤러
| 컨트롤러 (`Satis/sys/controller/Project/`) | 대응 View (`Smarty/templates/Satis/Project/`) | 주요 기능 |
| :--- | :--- | :--- |
| `Project_Controller.php` | `Project.tpl` | 프로젝트 마스터 등록/관리 |
| `ProjectOrderContract_Controller.php` | `ProjectOrderContract.tpl` | 수주 계약, 변경 계약, 공동도급 지분율 |
| `ProjectOrderGisung_Controller.php` | `ProjectOrderGisung.tpl` | 기성 청구 / 수금 / 미수금 |
| `ProjectMenHour_Controller.php` | `ProjectMenHour.tpl` | M/M 공수 집계 및 원가 배부 |
| `ProjectCostStatus_Controller.php` | `ProjectCostStatus.tpl` | 투입 원가(인건비/직접비/경비) 현황 |
| `ProjectCostTotal_Controller.php` | `ProjectCostTotal.tpl` | 원가 합계 현황 |
| `ProjectPrePayment_Controller.php` | `ProjectPrePayment.tpl` | 선급금 신청/수금/정산 |
| `ProjectStatus_Controller.php` | `ProjectStatus.tpl` | 프로젝트 진행 현황 |
| `ProjectBaseCode_Controller.php` | `ProjectBaseCode.tpl` | 프로젝트 기본 코드 관리 |
| `ProjectPartner_Controller.php` | `ProjectPartner.tpl` | 협력업체 관리 |
| `DeptAssessment_Controller.php` | `DeptAssessment.tpl` | 부서 평가 관리 |
| `TargetManagement_Controller.php` | `TargetManagement.tpl` | 수주 목표 관리 |
| `PrinterCopy_Controller.php` | `PrinterCopy.tpl` | 인쇄 출력 처리 |

### 1.4 비즈니스 로직 (Model 계층: `Satis/sys/model/*`) 20개 모듈 명세

Model 계층은 MVC 아키텍처 중 비즈니스 데이터 처리 및 DB 저장 프로시저(SP) 파라미터 구성, 결과 데이터 가공을 담당합니다. 모든 Logic 클래스는 생성자에서 `$this->oracle = new OracleClass($smarty)`를 초기화하여 데이터베이스에 접근합니다.

#### A. 모듈별 대표 Logic 클래스 목록

| 모듈 디렉토리 (`Satis/sys/model/`) | 포함 Logic 파일 수 | 주요 Logic 클래스명 (`*_logic.php`) | 핵심 역할 및 기능 |
| :--- | :---: | :--- | :--- |
| **Account** | 41개 | `AccountSlipTemp_logic.php`, `AccountSlipConfirmed_logic.php`, `AccountGisungCalculation_logic.php`, `FinalClosing_logic.php` | 임시/확정 전표 처리, 기성 청구금액 계산, 원가 배부, 결산 처리 |
| **Person** | 36개 | `PersonInfoManager_logic.php`, `PersonPaymentCalculation_logic.php`, `PersonYearEndTax_logic.php`, `PersonAppointment_logic.php` | 인사기록, 급여/상여 산출 엔진, 연말정산(`yeta_data`), 인사 발령 |
| **Project** | 16개 | `Project_logic.php`, `ProjectOrderContract_logic.php`, `ProjectMenHour_logic.php`, `ProjectOrderGisung_logic.php` | 프로젝트 마스터, 수주/변경 계약, M/M 공수 투입 집계, 기성 수금 |
| **Document** | 1개 | `Document_logic.php` | 전자결재 서식 생성, 문서 양식 데이터 파싱 |
| **Sign** | 6개 | `SignLogic.php`, `SignDocumentLogic.php` | 전자서명 문서 생성, 결재선 저장, 결재 상태 검증 및 서명 렌더링 |
| **Sale** | 9개 | `SaleBid_logic.php`, `SaleContractManager_logic.php`, `SalePerformanceManager_logic.php` | 영업 입찰, 수주 계약, 수금 및 수주 실적 관리 |
| **Purchase** | 4개 | `PurchaseClaim_logic.php`, `PurchaseOrder_logic.php`, `PurchaseExactCalculation_logic.php` | 구매 신청, 발주 처리, 대금 정산 |
| **Travel** | 1개 | `Travel_logic.php` | 국내/해외 출장 신청 및 여비 정산 처리 |
| **Asset** | 3개 | `AssetInfo_logic.php`, `AssetPurchase_logic.php`, `AssetBasicCode_logic.php` | 고정자산 등록, 자산 구매, 자산 코드 관리 |
| **Book** | 1개 | `Book_logic.php` | 도서 목록 등록 및 대여/반납 처리 |
| **Association**| 1개 | `Association_logic.php` | 엔지니어링 협회 회원 관리 및 수수료 정산 |
| **Management** | 4개 | `CommonCode_logic.php`, `MenuAuth_logic.php`, `Notify_logic.php` | 시스템 공통 코드, 사용자 메뉴 권한, 알림 팝업 |
| **Supervisor** | 2개 | `Supervisor_logic.php` | 시스템 전체 공정코드, 부서코드, 권한 마스터 관리 |
| **Common** | 17개 | `sms_logic.php`, `PdfLogic.php`, `popup_logic.php`, `work_logic.php` | SMS 발송 연동, PDF 출력 변환, 공통 팝업 데이터 공급 |
| **Jqgrid** | 1개 | `Jqgrid_logic.php` | jqGrid 요청에 대응하는 JSON 데이터 바인딩 로직 |
| **Fileupload** | 8개 | `Fileupload_logic.php`, `AddCode01_upload_insa_image.php` | 첨부파일 업로드/다운로드, 인사 사진 업로드 및 삭제 |
| **Help** | 1개 | `Help_logic.php` | 시스템 사용 도움말 및 Q&A 로직 |
| **Report** | 1개 | `Report_logic.php` | 경영진 보고서 및 엑셀 보고서 데이터 가공 |
| **Sample** | 1개 | `Sample01_logic.php` | 표준 개발용 샘플 로직 |
| **TestLogic** | 1개 | `TestLogic.php` | 개발자 테스트 전용 로직 |

#### B. Model (Logic) 클래스의 공통 설계 구조 패턴

1. **초기화 및 의존성 주입**:
   ```php
   class AccountSlipTempLogic {
       var $oracle;
       var $smarty;

       function AccountSlipTempLogic($smarty) {
           $this->smarty = $smarty;
           $this->oracle = new OracleClass($smarty); // DB 객체 인스턴스화
       }
   }
   ```

2. **Controller 파라미터 바인딩 및 프로시저 호출**:
   * Controller의 `$ActionMode` 요청에 따라 Logic 클래스 내 동명의 메소드가 실행됩니다.
   * 입력 파라미터를  sanitize 및 인코딩 변환(`HangleEncodeUTF8_EUCKR`) 후 Oracle 저장 프로시저 호출 구문(`BEGIN usp_...; END;`)을 완성합니다.
   * `LoadProcedure_new()` 또는 `executeSelect()`를 호출하여 결과를 Smarty 뷰로 전달합니다.

3. **파일 업로드 및 이미지 관리 로직 (`Fileupload_logic.php` & `AddCode01_...`)**:
   * 인사기록카드 사원 사진 업로드 시 `AddCode01_upload_insa_image.php`를 통해 이미지를 서브 디렉터리(`erpphoto/`)로 이송하고 파일명을 사번(`MemberNo.jpg`)으로 표준화합니다.
   * 파일 업로드 시 확장자 및 용량 제한 검증을 수행하고, Oracle DB에 첨부파일 메타데이터를 저장합니다.

---

## 2. 핵심 클래스 전체 메소드 명세 (OracleClass.php — 1,392라인)

`Satis/sys/util/OracleClass.php`는 시스템 전체 Oracle DB 연동의 핵심 클래스입니다.  
실제 소스 분석 기반의 **전체 메소드 분류표**입니다.

### 2.1 DB 커넥션 및 기본 쿼리 메소드

| 메소드명 | 설명 |
| :--- | :--- |
| `OracleClass($smarty)` | 생성자. OCI 커넥션 생성. `oci_connect('satis', '************', 'samandb')` |
| `db_close_oracle()` | 커넥션 명시적 해제. `oci_close()` |
| `Query($i_azsql)` | SQL 파싱 및 실행 후 Statement 핸들 반환. |
| `fetch_array($stmt)` | `oci_fetch_array($stmt, OCI_BOTH)` — 연관+인덱스 배열 동시 반환 |
| `ExcuteQuery($i_azsql)` | DML 단순 실행 (반환값 없음) |
| `LoadSql($i_azsql)` | 레거시 `ociparse/ociexecute/ocifetchinto` API 기반 단일 컬럼 조회 |
| `LoadConfigData($i_azsql)` | 설정 데이터를 `name=value` 포맷으로 직접 `echo` 출력 |
| `__destruct()` | PHP 소멸자: Statement 자동 해제 + 커넥션 자동 close |

### 2.2 Stored Procedure (저장 프로시저) 조회 메소드

| 메소드명 | 인자 | 반환 방식 | 특이사항 |
| :--- | :--- | :--- | :--- |
| `LoadProcedure($sql, $title, $short_array)` | SQL, Smarty 변수명 | `$query_data` 배열 + Smarty assign | 기본 프로시저 호출 메소드 |
| `LoadProcedure_old($sql, $title, $short_array)` | SQL, Smarty 변수명 | `$query_data` 배열 | `$_short` 축약형 컬럼 지원 (구버전) |
| `LoadProcedure_new($sql, $title, $short_array, $return_type)` | SQL, 변수명, 반환 타입 | `array` / `json` / `jsonchart` / `print_r` / Smarty | 출력 타입 지정 가능 |
| `LoadProcedure_new2($sql, $title, $short_array, $return_type)` | SQL, 변수명, 반환 타입 | 단일 Row 전용 | 단건 조회용 최적화 |
| `LoadProcedure_empty($sql, $title, $load_type, $return_type)` | SQL, 변수명, 로드타입 | `$query_data` 배열 | `w2ui` 모드 시 `recid` 자동 부여 |
| `LoadProcedure_broken_cp949($sql, $title, $load_type, $return_type)` | SQL, 변수명, 로드타입 | `$query_data` 배열 | CP949 인코딩 깨짐 데이터 처리용 |
| `LoadProcedureAjax($sql)` | SQL | `print_r(urldecode(json_encode($data)))` | Ajax 요청 전용 JSON 직접 출력 |
| `LoadProcedureApi($sql, $output_type)` | SQL, 출력 타입 | `array` / `json` / `jsonchart` | API/외부 호출 전용 |
| `Get_ProcedureData($Param01~05)` | 최대 5개 파라미터 | `array` | `usp_get_procedure_data` 프로시저 공통 호출 래퍼 |
| `executeSelect($sql)` | SQL | 구조화된 결과 객체 (`success`, `rows`, `columns`, `error`) | 최신 트랜잭션 안전 조회 함수 (2025-07-25 추가) |

### 2.3 DML 실행 메소드 (INSERT / UPDATE / DELETE)

| 메소드명 | 설명 | 에러 반환 방식 |
| :--- | :--- | :--- |
| `ProcedureExcuteQuery($sql)` | 프로시저 DML 실행, 에러 시 `echo` | `echo '-----'.$err['message']` |
| `ProcedureExcuteQuery_new($sql, $encode_type, $return_type)` | 인코딩 선택(`utf8`/`euckr`) 후 DML 실행 | echo 출력 |
| `ProcedureExcuteQuery_boolean($sql)` | DML 실행 후 `true`/`false` 반환 | boolean |
| `ProcedureExcuteQuery_boolean2($sql)` | DML 실행 후 `result`/`error` 구조체 반환 | 배열 구조체 |
| `ProcedureExcuteQuery_ErrorCheck($sql, $memberId, $date, $returnYN, $encode_type, $output_msg)` | 프로시저 실행 후 `Common_Confirm_Error` 프로시저로 이중 에러 검증 | `echo` 또는 `return` |
| `ProcedureExcuteQuery_Return($sql, $ReturnContent)` | DML 후 `RESULT_CODE` / `RESULT_MSG` / `ALL` 선택 반환 | 문자열 또는 배열 |
| `ProcedureExcuteQuery_ExcReturn($sql, $ReturnContent)` | 최신 try-catch 기반 DML. Oracle 에러코드 한국어 메시지 매핑 | JSON 또는 한국어 에러 문자열 |
| `executeDml($sql)` | 트랜잭션 안전 DML 실행 (2025-07-25 신규 추가) | 구조화된 결과 객체 |
| `ProcedureExcuteQuery_ErrorPrint($sql, $encode_type, $return_type)` | 에러 발생 시 콘솔 출력 | `echo` |

### 2.4 Oracle 에러코드 → 한국어 메시지 매핑 테이블

`ProcedureExcuteQuery_ExcReturn()` 내부의 `$ErrorKind_Array`로 Oracle ORA- 에러 코드를 사용자 친화적 한국어 메시지로 변환합니다.

```php
$ErrorKind_Array = [
    "-1"     => "존재하는 내용입니다. 확인 후 입력해 주세요.",         // ORA-00001: DUP_VAL_ON_INDEX
    "-1400"  => "빈값을 입력 할 수 없습니다.",                       // NOT NULL 위반
    "-1722"  => "숫자자리에 문자를 입력할 수 없습니다.",               // INVALID_NUMBER
    "-1407"  => "빈값을 입력 할 수 없습니다.",                       // NOT NULL 위반 (다른 컬럼)
    "-1438"  => "숫자값이 너무 커서 입력할 수 없습니다.",              // 숫자 정밀도 초과
    "-12899" => "글자가 너무 길어 입력할 수 없습니다.",               // VARCHAR2 길이 초과
    "-6550"  => "필수항목이 입력되지 않았습니다.",                    // PL/SQL 컴파일 에러
    "-6511"  => "(CURSOR_ALREADY_OPEN) : 커서가 이미 OPEN 상태",
    "-6502"  => "(VALUE_ERROR) : 수치 또는 값 오류",
    "-1476"  => "(ZERO_DIVIDE) : 0으로 나눌때",
    "-1017"  => "(LOGIN_DENIED) : 잘못된 사용자 이름이나 비밀번호로 로그인 시도",
    "-1422"  => "(TOO_MANY_ROWS) : SELECT INTO 결과가 두 건 이상",
];
```

### 2.5 문자 인코딩 변환 메소드 (CP949 ↔ UTF-8 파이프라인)

Oracle DB는 `CP949 (EUC-KR)` 인코딩을 사용하고, 웹 화면은 `UTF-8`로 구성되어 있어 양방향 변환이 필수입니다.

```mermaid
graph LR
    DB["Oracle DB (CP949/EUC-KR)"]
    PHP["PHP Application (UTF-8)"]
    Browser["Web Browser (UTF-8)"]

    DB -->|HangleEncode: ICONV cp949→UTF-8| PHP
    PHP -->|HangleEncodeUTF8_EUCKR: ICONV UTF-8→cp949| DB
    PHP -->|HangleEncodeAjax: cp949→UTF-8 Ajax 전용| Browser
```

| 메소드명 | 변환 방향 | 비고 |
| :--- | :--- | :--- |
| `HangleEncode($item)` | `CP949 → UTF-8` | 공백은 `&nbsp`로 대체 |
| `HangleEncode_empty($item)` | `CP949 → UTF-8` | 공백 대체 없음 |
| `HangleEncodeAjax($item)` | `CP949 → UTF-8` | Ajax 응답 전용 |
| `HangleEncodeUTF8_EUCKR($item)` | `UTF-8 → CP949` | SQL 삽입 시 한글 보호 |
| `bear3StrCut($str, $len, $tail)` | — | 멀티바이트 문자열 정규식 자르기 (`preg_match`) |
| `resizeString($str, $size, $addStr)` | — | `mb_strlen/mb_substr` 기반 UTF-8 자르기 |

### 2.6 로그 기록 메소드

```php
function OracleLogWrite($msg, $menu) {
    // 로그 파일 경로: Satis/sys/log/YYYY-MM-DD_{메소드명}.txt
    $cfile = "../../log/" . date("Y-m-d") . "_" . $menu . ".txt";

    // 기존 파일 내용 읽기 → 새 내용 추가 → 파일 쓰기
    if (file_exists($cfile)) {
        $fd = fopen($cfile, 'r');
        $con = fread($fd, filesize($cfile));
        fclose($fd);
    }
    $fp = fopen($cfile, 'w');
    $cond = $con . date("Y-m-d H:i:s") . "  " . $msg . "\n";
    fwrite($fp, $cond);
    fclose($fp);
}
```

> **로그 파일 명명 규칙**: `YYYY-MM-DD_{호출된메소드명}.txt`  
> 예: `2026-07-20_LoadProcedure.txt`, `2026-07-20_ExcuteQuery_new.txt`

### 2.7 최신 트랜잭션 안전 메소드 (2025-07-25 추가, 김병철)

```php
function executeSelect($sql) {
    try {
        set_error_handler(array($this, 'custom_error_handler'));
        $stmt = oci_parse($this->connection, $this->HangleEncodeUTF8_EUCKR($sql));

        // Ref Cursor 포함 여부 자동 감지
        if (strpos($sql, ":entries") !== false) {
            $entries = oci_new_cursor($this->connection);
            oci_bind_by_name($stmt, ":entries", $entries, -1, OCI_B_CURSOR);
            oci_execute($stmt, OCI_DEFAULT);
            oci_execute($entries);
            $fetchResult = $this->fetchResults($entries, $result);
        } else {
            oci_execute($stmt, OCI_DEFAULT);
            $fetchResult = $this->fetchResults($stmt, $result);
        }
        $result["rows"] = $fetchResult[0];
        $result["columns"] = $fetchResult[1];

    } catch (Exception $e) {
        oci_rollback($this->connection);  // 에러 시 자동 롤백
        $this->makeErrorResult($result, $stmt, $e->getMessage());

        // Oracle 에러코드 20000 이상 = 사용자 정의 예외 → DB 에러로그 삽입
        if ($result["error"]["datas"]["oci_error_data"]["code"] >= 20000) {
            $this->insertSmErrorLog($_SESSION["satis_user_id"], $errorMessage);
        }
    }
    restore_error_handler();
    return $result;
}
```

---

## 3. 전자결재 엔진 알고리즘 (`Satis/inc/approval_function.php` — 1,224라인)

### 3.1 전자결재 상태 머신 (State Machine)

```mermaid
stateDiagram-v2
    [*] --> CREATE : 기안 문서 작성 (DOC_STATUS_CREATE)

    CREATE --> TEMP   : 임시저장 (TEMP-00001)
    CREATE --> APPROVE: 결재 상신 (PROCESS_APPROVE=1)
    TEMP   --> APPROVE: 상신 실행

    state APPROVE {
        [*] --> DEPT   : 부서내결재 (부서내 / PROCESS_CODE)
        DEPT  --> RECV : 부서접수 (RECEIVE)
        RECV  --> SANCTION: 결의부서내 (SANCTION_CODE)
        SANCTION --> FINISH_LOOP: 처리부서내 (SANCTION_CODE2)
    }

    APPROVE --> BACK     : 이전 결재자 반송 (PROCESS_BACK="BACK")
    APPROVE --> RETURN   : 부결/반송 (PROCESS_RETURN="RETURN")
    APPROVE --> FINISH   : 최종 승인 (PROCESS_FINISH="FINISH")
    APPROVE --> DECISION : 전결 (PROCESS_DECISION="FINISH-DECISION")

    BACK   --> APPROVE : 재결재
    RETURN --> CREATE  : 재기안
    FINISH   --> [*]
    DECISION --> [*]
```

### 3.2 전체 상태 상수 정의 (`approval_var.php`)

```php
/* 결재 진행 상태 코드 */
$PROCESS_APPROVE    = 1;               // 결재진행중
$PROCESS_TEMPORARY  = "TEMP-";        // 임시저장 (형식: TEMP-00001 또는 TEMP-00001-T02211)
$PROCESS_ACCEPT     = "ACCEPT";       // 가결
$PROCESS_REJECTION  = "REJECT";       // 부결 (기능삭제됨)
$PROCESS_RETURN     = "RETURN";       // 반송 (재기안 가능)
$PROCESS_BACK       = "BACK";         // 반송 - 이전 결재자에게 재전달
$PROCESS_FINISH     = "FINISH";       // 최종 결재완료
$PROCESS_DECISION   = "FINISH-DECISION"; // 전결 처리
$PROCESS_RECEIVE    = "RECEIVE";      // 부서접수 대기
$PROCESS_CANCEL     = "CANCEL";       // 기안취소
$PROCESS_DELETE     = "DELETE";       // 기안 삭제

/* 문서 편집 상태 코드 */
$DOC_STATUS_CREATE  = "CREATE";   // 새 기안 문서 작성
$DOC_STATUS_EDIT    = "EDIT";     // 기안 문서 편집
$DOC_STATUS_VIEW    = "VIEWER";   // 기안 문서 보기 (편집불가)
$DOC_STATUS_APPROVE = "APPROVE";  // 결재함 문서 결재하기
$DOC_STATUS_ACCEPT  = "ACCEPT";   // 부서 문서 접수하기

/* 결재 라인 위치 코드 */
$PROCESS_CODE    = "부서내";           // 부서내 결재 진행 중
$TEMPORARY_CODE  = "부서내임시저장";   // 임시저장 코드
$SANCTION_CODE   = "결의부서내";       // 결의부서 결재 진행 중
$SANCTION_CODE2  = "처리부서내";       // 처리부서 결재 진행 중
$STEP_NO         = 9;                  // 최대 결재 단계 수
```

### 3.3 문서 번호 채번 알고리즘 (Serial Number Generation)

결재 상신 시 문서번호가 자동으로 채번되는 두 가지 방식이 있습니다.

#### A. 임시저장 문서번호 (`TempSerialNo` / `TempSerialNo2`)

```php
// 형식: "TEMP-00001" 또는 "TEMP-00001-T02211"
function TempSerialNo() {
    // SanctionDoc_tbl에서 TEMP-로 시작하는 마지막 번호 조회
    $res04 = mysql_query("SELECT * FROM SanctionDoc_tbl WHERE DocSN LIKE 'TEMP-%' ORDER BY DocSN DESC LIMIT 0,1", $db);
    if (mysql_num_rows($res04) > 0) {
        $aa = mysql_result($res04, 0, "DocSN");
        $bb = split("TEMP-", $aa);
        return "TEMP-" . sprintf("%05d", ($bb[1] + 1)); // 5자리 zero-padding
    } else {
        return "TEMP-" . sprintf("%05d", 1); // 최초: TEMP-00001
    }
}

// TempSerialNo2: 사번 suffix 포함 형식 (TEMP-00001-T02211)
// split("-", "TEMP-00021-T02211") → $bb[1] = "00021"
```

#### B. 결재 상신 문서번호 (`NewSerialNo` / `NewSerialNo2`)

```php
// 형식: "2026-00001" 또는 "2026-00001-T02211"
function NewSerialNo() {
    $NewKey = date('Y-'); // "2026-"
    $res04 = mysql_query("SELECT * FROM SanctionDoc_tbl WHERE DocSN LIKE '$NewKey%' ORDER BY DocSN DESC LIMIT 0,1", $db);
    if (mysql_num_rows($res04) > 0) {
        $aa = mysql_result($res04, 0, "DocSN");
        $bb = split($NewKey, $aa); // "2026-00005" → $bb[1] = "00005"
        return $NewKey . sprintf("%05d", ($bb[1] + 1)); // "2026-00006"
    } else {
        return $NewKey . sprintf("%05d", 1); // 최초: "2026-00001"
    }
}
```

### 3.4 결재선 순회 알고리즘 (`NextSanctionState`)

결재 진행 시 현재 결재 위치를 다음 결재자로 이동시키는 핵심 알고리즘입니다.

```php
function NextSanctionState($Tmp_Sanction, $Tmp_SanctionState) {
    // $Tmp_Sanction = "A001:B002:RECEIVE:C003:D004:FINISH" (결재선 배열, ':' 구분)
    // $Tmp_SanctionState = "1:부서내:A001:2026-07-20" (현재 단계:코드:사번:날짜)
    
    $TmpState = split(":", $Tmp_SanctionState);
    $Start = $TmpState[0]; // 현재 결재 인덱스

    // RECEIVE 위치(index) 먼저 탐색
    for ($i = $Start; $i < count($TmpArr); $i++) {
        if ($TmpArr[$i] == $PROCESS_RECEIVE) {
            $RECEIVE_INDEX = $i; break;
        }
    }

    // 다음 결재자 상태 결정
    for ($i = $Start; $i < count($TmpArr); $i++) {
        switch ($TmpArr[$i]) {
            case $PROCESS_RECEIVE:  // 부서접수로 넘김
                $Res_SanctionState = ($i+1).":".$PROCESS_RECEIVE.":".$TmpState[2].":".date('Y-m-d');
                break;
            case $PROCESS_FINISH:   // 결재 완료
                $Res_SanctionState = ($i+1).":".$PROCESS_FINISH.":".$TmpState[2].":".date('Y-m-d');
                break;
            default:                // 다음 결재자에게 넘김
                // RECEIVE 이후면 처리부서내(SANCTION_CODE2), 이전이면 결의부서내(SANCTION_CODE)
                $SANCTION_CODE_ = ($i > $RECEIVE_INDEX) ? $SANCTION_CODE2 : $SANCTION_CODE;
                $Res_SanctionState = ($i+1).":".$SANCTION_CODE_.":".$TmpArr[$i].":".date('Y-m-d');
                break;
        }
        break; // 첫 번째 유효 결재자만 처리
    }
    return $Res_SanctionState;
}
```

### 3.5 결재 서명/인장 렌더링 알고리즘 (`FindSanctionState` / `FindSanctionState2`)

결재 문서 화면에서 결재자 서명 이미지(인장) 또는 텍스트를 표시하는 렌더링 로직입니다.

```php
function FindSanctionState2($TmpSN, $sName, $sGroup) {
    // SanctionState_tbl에서 해당 결재자의 결재 이력 조회
    $azSQL = "SELECT * FROM SanctionState_tbl WHERE DocSN='$TmpSN' AND MemberNo LIKE '%$sName%'";
    
    $SanctionState = mysql_result($res06, 0, "SanctionState");
    $src_photo = "../../../erpphoto/$MemberNo.jpg"; // 결재자 서명 사진 경로

    // 대리결재 감지
    if ($sGroup == "결의부서내") {
        if (strpos(mysql_result($res06, 0, "MemberNo"), "대결") !== false) {
            $Member_Name .= "-대결"; // 대리결재 표시
        }
    }

    if ($SanctionState == "ACCEPT" || $SanctionState == "FINISH-DECISION") {
        if (file_exists($src_photo)) {
            // 서명 이미지 존재 시: <img src='erpphoto/A001.jpg' height=30>
            return "<img src='$src_photo' height=30><br>$Member_Name<br>($SanctionDate)";
        } else {
            // 서명 없음 시: 빨간색 텍스트("승인" 또는 "전결")
            $ok_str = ($SanctionState == "ACCEPT") ? "승인" : "전결";
            return "<font color=red>$ok_str</font><br><br>$Member_Name<br>($SanctionDate)";
        }
    } else {
        return "<br><br>$Member_Name<br>(미결)"; // 아직 결재 안 됨
    }
}
```

### 3.6 공통 코드 룩업 함수 목록 (`approval_function.php`)

전자결재 처리 중 MySQL `intranet_tbl` / `member_tbl` / `SystemConfig_Tbl`을 조회하는 헬퍼 함수 목록입니다.

| 함수명 | 조회 테이블 | 반환값 |
| :--- | :--- | :--- |
| `MemberNo2Ip($mCode)` | `intranet_tbl` | 사번으로 IP 주소 반환 (끝자리 0/1이면 공백) |
| `MemberNo2BossIP($Tmp_Sanction, $Step)` | `intranet_tbl` | 결재선 특정 스텝의 결재자 IP |
| `MemberNo2Name($mCode)` | `member_tbl` | 사번 → 한글 성명 |
| `MemberNo2Rank($mCode)` | `member_tbl`, `SystemConfig_Tbl` | 사번 → 직급명 |
| `MemberNo2GroupCode($mCode)` | `member_tbl` | 사번 → 부서코드 (2자리 zero-pad) |
| `MemberNo2GroupName($mCode)` | `member_tbl`, `SystemConfig_Tbl` | 사번 → 부서명 |
| `MemberNo2SiteName($mCode)` | `systemconfig_tbl` | 사번 → 현장명 (TeamLeader 기준) |
| `DocCode2Name($DocCode)` | `systemconfig_tbl` | 결재 서식 코드 → 서식명 |
| `ProjectCode2Name($mCode)` | `project_tbl` | 프로젝트 코드 → 약칭 |
| `ProjectCode2Bridge($mCode, $no)` | `project_cs_summary_tbl` | 교량 프로젝트 약칭 |
| `Code2Name($mCode, $dbSysKey, $Kind)` | `SystemConfig_Tbl` | 일반 코드 → 명칭 (`Kind=0`) 또는 설명 (`Kind=1`) |
| `Group2Manager($mCode)` | `SystemConfig_Tbl` | 부서코드 → 부서장 사번 (`Note` 컬럼) |
| `ProcessingState($TmpSN, $tmpState, $mCode)` | `SanctionDoc_tbl` | 현재 결재 상태를 한국어 명칭으로 변환 |
| `FindSanctionDate($TmpSN, $TmpMemberNo)` | `SanctionState_tbl` | 특정 결재자의 결재일자 반환 |
| `TempState($mCode)` | — | 임시저장 상태 문자열 생성 (`0:부서내임시저장:A001:YYYY-MM-DD`) |

---

## 4. 시스템 글로벌 변수 및 초기화 파일 (`DefaultInformation.php`)

모든 컨트롤러에서 `include "../../util/DefaultInformation.php"`로 로드되는  
**시스템 기본 정보 세션 초기화 파일**입니다.

```php
/* 화면 요청 정보 */
$smarty->assign('auth_page_id',    $_REQUEST['target_id']);
$smarty->assign('auth_page_devYN2', 'Y');
$smarty->assign('satis_user_id',    $_SESSION['satis_user_id']);

/* 개발자 화이트리스트 (devYN='Y' 권한 부여) */
$devUserList = [
    "203155" => "한정규",
    "216070" => "정명준",
    "209171" => "신지호",
    "216011" => "장계석",
    "221064" => "김병철",
    "223158" => "김진선",
];

/* 로그인 사용자가 개발자 목록에 있으면 devYN='Y' 설정 */
if (array_key_exists($_SESSION['satis_user_id'], $devUserList)) {
    $smarty->assign('auth_page_devYN', 'Y');
}

/* 회사 기본 정보 */
$site_code = "SAMAN";           // 회사 영문 식별 코드
$site_name = "삼안";
$Corporation_name = "(주)삼안";
$company_registration_number = "211-81-25425"; // 사업자등록번호
$smarty->assign('site_code', $site_code);
$smarty->assign('site_name', $site_name);
$smarty->assign('Corporation_name', $Corporation_name);
$smarty->assign('company_registration_number', $company_registration_number);
```

---

## 5. 공통 헬퍼 함수 라이브러리 (`Satis/inc/function.php` — 1,093라인)

| 함수명 | 설명 |
| :--- | :--- |
| `str_cutting($str, $len)` | 멀티바이트 문자열 자르기 (한글 2바이트 처리, 생략 시 `...` 추가) |
| `substr1($str, $len)` | str_cutting의 Trailing 없는 단순 잘라내기 버전 |
| `ConfigName($category, $code)` | `config_tbl` 코드 → 명칭 반환 |
| `ConfigName2($category, $code)` | `config_tbl` 코드 → 설명(desc) 반환 |
| `Code2Name($mCode, $dbSysKey, $Kind)` | `SystemConfig_Tbl` 일반 코드 → 명칭 반환 |
| `change_code($pcode)` | 연도 코드 `XX`를 실제 연도(2자리)로 치환 |

---

## 6. 데이터베이스 스키마 상세 (MySQL 인트라넷 DB — `hallaerp`)

`approval_function.php`의 실제 SQL 쿼리 분석으로 도출한 인트라넷 MySQL DB 테이블 구조입니다.

| 테이블명 | 주요 컬럼 | 역할 |
| :--- | :--- | :--- |
| `member_tbl` | `MemberNo`, `KorName`, `RankCode`, `GroupCode`, `WorkPosition` | 사원 기본 정보 (`WorkPosition='9'` = 퇴직자) |
| `intranet_tbl` | `MemberNo`, `UseIP` | 사원 네트워크 IP 정보 |
| `SanctionDoc_tbl` | `DocSN`, `DocCode`, `PG_Date`, `DocTitle`, `DocStatus` | 전자결재 문서 마스터 |
| `SanctionState_tbl` | `DocSN`, `MemberNo`, `SanctionOrder`, `SanctionState`, `SanctionDate` | 결재자별 결재 이력 |
| `SystemConfig_Tbl` | `SysKey`, `Code`, `Name`, `Description`, `Note` | 시스템 공통 코드 사전 (부서코드, 직급코드, 결재서식 등) |
| `project_tbl` | `ProjectCode`, `ProjectNickname` | 프로젝트 약칭 |
| `project_cs_summary_tbl` | `ProjectCode`, `no`, `Item` | 교량/구조물 프로젝트 세부 항목 |
| `config_tbl` | `category`, `code`, `name`, `desc` | 화면별 코드 설정 |

---

## 7. 예외 처리 & 보안 핸들링 상세 (`AuthClass.php` — 591라인)

### 7.1 CSRF 토큰 생성 알고리즘
```php
function Auth_TokenGenerater($assign_boolean = false) {
    // 62자 문자풀에서 10자리 난수 토큰 생성
    $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $randomString = '';
    for ($i = 0; $i < 10; $i++) {
        $randomString .= $characters[rand(0, 61)];
    }
    if ($assign_boolean) {
        $_SESSION['Token'] = $randomString;    // 서버 세션에 저장
        $this->smarty->assign('Token', $randomString); // 뷰로 전달 (히든 필드 렌더링)
    } else {
        return $randomString;
    }
}
```

### 7.2 토큰 검증 흐름
```mermaid
sequenceDiagram
    autonumber
    actor User
    participant Controller
    participant AuthClass
    participant Session

    User->>Controller: 폼 제출 (Token 파라미터 포함)
    Controller->>AuthClass: Auth_TokenChecker($Token, $page_move=true)
    AuthClass->>Session: $_SESSION['Token'] 조회
    alt 토큰 일치
        AuthClass->>Controller: Smarty assign('Token', ...) 후 정상 처리
    else 토큰 불일치
        AuthClass->>Session: session_destroy() 세션 파기
        AuthClass->>User: auth_move_page.tpl 출력 (로그인 페이지 이동)
        AuthClass->>Controller: exit() 강제 종료
    end
```

### 7.3 세션 변수 목록

| 세션 키 | 설명 |
| :--- | :--- |
| `$_SESSION['satis_user_id']` | 로그인 사원번호 (예: `"221064"`) |
| `$_SESSION['satis_user_name']` | 로그인 사원 성명 |
| `$_SESSION['satis_user_deptcode']` | 사원 부서코드 |
| `$_SESSION['satis_user_deptname']` | 사원 부서명 |
| `$_SESSION['Token']` | CSRF Anti-Forgery Token (10자리 난수) |
| `$_SESSION['Title']` | 현재 화면 제목 |
| `$_SESSION['Auth']` | 권한 레벨 |
| `$_SESSION['Type']` | 사용자 유형 |

---

## 8. 핵심 소스 파일 사전 (Source File Dictionary — 전체)

| 파일 경로 | 용량 / 라인 | 핵심 역할 |
| :--- | :--- | :--- |
| `Satis/sys/util/OracleClass.php` | 46KB / 1,392라인 | Oracle OCI 커넥션, Ref Cursor 조회/DML, 인코딩 변환 |
| `Satis/sys/util/AuthClass.php` | 18KB / 591라인 | 세션 토큰 생성/검증, CSRF 방어 |
| `Satis/sys/util/CommonCodeList.php` | 294KB | 시스템 공통 코드 PHP 배열 인메모리 캐시 |
| `Satis/inc/approval_function.php` | 35KB / 1,224라인 | 전자결재 상태 처리, 결재선 순회, 서명 렌더링 |
| `Satis/inc/function.php` | 30KB / 1,093라인 | 문자열 처리, 코드룩업, 공통 헬퍼 함수 |
| `Satis/inc/function_intranet.php` | 33KB | 인트라넷 전용 헬퍼 함수 |
| `Satis/inc/approval_var.php` | 1.6KB / 29라인 | 전자결재 상태 상수 정의 |
| `Satis/inc/dbcon.inc` | 0.4KB | 인트라넷 MySQL 커넥션 (`erp.samaneng.com/hallaerp`) |
| `Satis/inc/saman_icube_dbcon.inc` | 0.5KB | 더존 iCube MS-SQL 커넥션 (`192.168.13.50:5539/DZICUBE`) |
| `Satis/sys/util/DefaultInformation.php` | 1.3KB | 시스템 글로벌 변수, 개발자 권한, 회사 정보 초기화 |
| `SmartyConfig.php` | 0.3KB | Smarty 엔진 경로 설정 |
| `Satis/sys/util/PrintAgent.php` | 1KB | 인쇄 에이전트 유틸리티 |
| `Satis/sys/util/ReportViewExcel.php` | 2.8KB | Excel 보고서 출력 처리 |

---

## 9. M/M (Man-Month) 투입 공수 및 원가 배부 공식

엔지니어링 업계 특성상 프로젝트별 인건비 원가 배부는 투입 M/M 비율을 기준으로 계산됩니다.

$$\text{Project}_{i} \text{ 원가 배부액} = \text{직원 월 급여} \times \left( \frac{\text{Project}_{i} \text{ 투입 M/M}}{\displaystyle\sum_{j=1}^{n} \text{Project}_{j} \text{ 투입 M/M}} \right)$$

**처리 흐름**:
1. `PersonPaymentCalculation`: 직원의 당월 급여 총액 및 4대보험 회사 부담금 계산
2. `ProjectMenHour`: 해당 월 직원의 프로젝트별 투입 M/M 입력
3. Oracle 저장 프로시저: 비율 계산 → 각 프로젝트 원가 테이블 자동 배부
4. `AccountGisungCalculation`: 프로젝트 기성 청구 시 누적 원가 반영

---

## 10. 로우레벨 개발 시 주의사항

> [!CAUTION]
> **SQL Injection 위험**: `approval_function.php` 및 `function.php`의 MySQL 함수들은 대부분 `$mCode` 값을 필터링 없이 SQL에 직접 삽입합니다. 수정 시 반드시 `mysql_real_escape_string()` 또는 PDO Prepared Statement로 교체해야 합니다.

> [!WARNING]
> **레거시 드라이버**: `mysql_connect()`, `mssql_connect()` 등 PHP 7.x에서 완전히 제거된 드라이버를 사용합니다. PHP 5.6 이하 환경에서만 실행 가능합니다.

> [!WARNING]
> **하드코딩된 개발자 화이트리스트**: `DefaultInformation.php`에 개발자 사번이 하드코딩되어 있습니다. 인사 이동 시 소스 직접 수정이 필요합니다.

> [!NOTE]
> **Oracle 트랜잭션**: `ProcedureExcuteQuery()`는 `OCI_COMMIT_ON_SUCCESS`(자동 커밋) 모드로 동작합니다. 여러 DML을 하나의 트랜잭션으로 묶으려면 `OCI_DEFAULT` 모드의 `executeDml()` / `executeSelect()`를 사용해야 합니다.

> [!NOTE]
> **Session 의존성**: 모든 컨트롤러 로직은 `session_start()` → `GetUserInfo.php` → `DefaultInformation.php` 순서로 초기화가 완료되어야 합니다. 순서가 바뀌면 `$_SESSION['satis_user_id']`가 null이 되어 개발자 권한 판별이 실패합니다.
