유지보수 인수인계 MD 작성
This commit is contained in:
+306
@@ -0,0 +1,306 @@
|
|||||||
|
# FTPDJ — 대외기관 파일 수신 어댑터 (인수인계 문서)
|
||||||
|
|
||||||
|
> **대상 소스**: [`src/com/eactive/eai/adapter/ftp/FTPDJ.java`](../src/com/eactive/eai/adapter/ftp/FTPDJ.java)
|
||||||
|
|
||||||
|
> **관련 가이드**: [`DJERP_1000_AP_BAP(FTP)개발가이드_v0.9_20260603.pptx`](./DJERP_1000_AP_BAP%28FTP%29개발가이드_v0.9_20260603.pptx) — EMS 등록 절차·화면 조작 중심
|
||||||
|
|
||||||
|
> **최종 갱신**: 2026-09-09
|
||||||
|
|
||||||
|
## 0. 이 문서에 대하여
|
||||||
|
|
||||||
|
FTPDJ를 **넘겨받아 유지보수할 개발자**를 위한 문서입니다. PPT 가이드가 "화면에서 어떻게 등록하는가"를 다룬다면, 이 문서는 **"등록한 값이 코드에서 어떻게 해석되어 동작하는가"** 를 다룹니다.
|
||||||
|
|
||||||
|
특히 FTPDJ의 동작은 4장~10장이 그 내용이며 이 문서의 핵심입니다.
|
||||||
|
|
||||||
|
값 검증이 없어 **오타가 조용히 다른 동작으로 이어지는 지점**이 몇 군데 있습니다. 6장(`protocol`)과 7장(`io.bandwidth`)은 반드시 읽고 넘어가십시오.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 저장소 · 빌드 · 배포
|
||||||
|
|
||||||
|
### 1.1 Git 저장소
|
||||||
|
|
||||||
|
```
|
||||||
|
origin https://git.eactive.synology.me:8090/eapim/djbank/bapweb.git
|
||||||
|
```
|
||||||
|
|
||||||
|
| 브랜치 | 용도 |
|
||||||
|
|---|---|
|
||||||
|
| `master` | 주 브랜치 |
|
||||||
|
| `soxor` | 작업 브랜치 (FTPDJ 개발이 진행된 곳) |
|
||||||
|
|
||||||
|
FTPDJ 관련 주요 커밋:
|
||||||
|
|
||||||
|
| 커밋 | 일자 | 내용 |
|
||||||
|
|---|---|---|
|
||||||
|
| `adeec70` | 2026-06-05 | `FileFetchRequestHandler` 구현 (HTTP 요청 진입점 신규) |
|
||||||
|
| `7f98f97` | 2026-09-07 | 취약점 patch version (의존 jar 교체) |
|
||||||
|
|
||||||
|
### 1.2 빌드
|
||||||
|
|
||||||
|
Ant 기반입니다. Maven/Gradle 설정은 없으며 **jar는 `WebContent/WEB-INF/lib`에 직접 커밋**되어 있습니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ant war # clean → compile → dist/BAPWeb.war 생성
|
||||||
|
ant build # 컴파일만 (build/classes)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 항목 | 값 |
|
||||||
|
|---|---|
|
||||||
|
| Java source/target | **17** (`build.xml`) |
|
||||||
|
| 컴파일 classpath | `WebContent/WEB-INF/lib/*.jar` (단 `ezgatormodule2.jar` 제외) + `libs/*.jar` |
|
||||||
|
| 산출물 | `dist/BAPWeb.war` |
|
||||||
|
| WAS | WebLogic, context-root `/BAPWeb` (`WebContent/WEB-INF/weblogic.xml`) |
|
||||||
|
|
||||||
|
> `WebContent/WEB-INF/oldlib`은 취약점 조치로 교체된 **구버전 jar 보관용**입니다. 클래스패스에도 WAR에도 포함되지 않습니다(`build.xml`의 war 태스크에서 제외 처리). 되돌릴 일이 없다면 삭제해도 무방합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. FTPDJ는 무엇인가
|
||||||
|
|
||||||
|
EMS(관리자포탈)의 **대외기관 등록 → 송수신구분코드**에서 `FTPDJ`를 선택하면 이 클래스가 전송 어댑터로 동작합니다.
|
||||||
|
DJBank 공용 SFTP 프록시를 경유해 **대외기관의 파일을 내려받는(Download) 전용 어댑터**입니다.
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 클래스 | `com.eactive.eai.adapter.ftp.FTPDJ` |
|
||||||
|
| 상속 / 구현 | `Transfer` 상속, `SocketService` 구현 |
|
||||||
|
| 지원 동작 | **수신(`recvFile`)만 지원** |
|
||||||
|
| 미지원 | `sendFile()` 호출 시 `UnsupportedOperationException` |
|
||||||
|
| 전송 라이브러리 | JSch 기반 (`FTPUtil.listFilesJSCH`, `FTPUtil.retrieve`) |
|
||||||
|
|
||||||
|
`SocketService`의 나머지 메서드(`connect`, `isActive`, `getCurrentSocket` 등)는 인터페이스 계약을 채우기 위한 빈 구현입니다. **실제 로직은 `recvFile()` 한 곳에서 시작합니다.** 소스를 처음 읽는다면 여기부터 보십시오.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 전체 동작 흐름
|
||||||
|
|
||||||
|
```
|
||||||
|
[1] HTTP 요청 (action=DOWNLOAD, remoteCode, remoteFile)
|
||||||
|
│ FileFetchRequestHandler
|
||||||
|
▼
|
||||||
|
[2] Job_Queue 테이블에 작업 등록 → 즉시 응답 (status=QUEUED, jobId=UUID)
|
||||||
|
│ 스케줄러가 매분 0초 / 30초에 큐를 집어감
|
||||||
|
▼
|
||||||
|
[3] FTPDJ.recvFile(batchDoc) ← 이 문서의 범위
|
||||||
|
│
|
||||||
|
├─ FtpEnv.init() : 접속정보(DB) + 프로퍼티(DB)로 실행 환경 조립 ← 핵심
|
||||||
|
├─ listFilesJSCH() : 원격 디렉터리 목록 조회 후 대상 파일 존재 확인
|
||||||
|
├─ fileDownProcess() : 다운로드 → RECVREAL 수신 → RECVARCH 이동 → check 파일 생성
|
||||||
|
└─ callbackProcess() : callback.url 이 있으면 BAT에 완료 통보 후 응답코드 검증
|
||||||
|
```
|
||||||
|
|
||||||
|
**비동기 구조**입니다. HTTP 요청은 큐에 넣고 바로 `QUEUED`를 반환하며, 실제 전송은 최대 30초 뒤 스케줄러가 시작합니다. 요청자는 `action=STATUS`로 `jobId`를 조회해 진행 상태를 확인합니다. 요청/응답 JSON 규격은 PPT 가이드 4장을 참조하십시오.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 프로퍼티 목록 (`TelegramInfo{업무구분_대외기관}`)
|
||||||
|
|
||||||
|
FTPDJ가 **실제로 읽는** 프로퍼티는 아래 8개가 전부입니다.
|
||||||
|
|
||||||
|
| 키 | 기본값 | 용도 | 비고 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `remote.path` | `""` | 원격지 기준 경로 | `#YYYYMMDD#` 치환 지원. 요청이 절대경로면 **무시됨** (5장) |
|
||||||
|
| `protocol` | `""` → `sftp_passwd` | 전송 프로토콜 | **값 매핑 주의** (6장) |
|
||||||
|
| `proxy.server` | `""` | SFTP 프록시 서버 | `IP:PORT` 형식. 비우면 직접 접속 |
|
||||||
|
| `connection.timeout` | `30` | 접속 타임아웃(초) | 숫자만. 문자 포함 시 `NumberFormatException` |
|
||||||
|
| `read.timeout` | `30` | 읽기 타임아웃(초) | 동일 |
|
||||||
|
| `io.bandwidth` | `0` | 전송 대역 제한 | `0`=무제한. `K`/`M` 표기 지원 (7장) |
|
||||||
|
| `ftp.transfer.type` | `BINARY` | FTP 전송 모드 | `ASCII`가 필요한 경우에만 지정 |
|
||||||
|
| `callback.url` | `""` | 수신 완료 통보 URL | 비우면 콜백 생략 (10장) |
|
||||||
|
|
||||||
|
접속 정보(IP·포트·사용자 ID·비밀번호/SSH Key)와 수신 대상 파일명은 **프로퍼티가 아니라 대외기관 연결 정보 및 HTTP 요청**에서 배치 헤더로 전달됩니다. 프로퍼티에서 찾지 마십시오.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. `remote.path` 조립 규칙
|
||||||
|
|
||||||
|
원격 경로는 **프로퍼티 단독으로 결정되지 않고** 요청으로 들어온 `remoteFile`과 조합됩니다.
|
||||||
|
|
||||||
|
```java
|
||||||
|
remotePath = 프로퍼티 remote.path
|
||||||
|
requestPath = remoteFile 의 디렉터리 부분
|
||||||
|
remoteFile = remoteFile 의 파일명 부분
|
||||||
|
|
||||||
|
remotePath = Paths.get(remotePath).resolve(requestPath).normalize() + "/"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.1 요청이 절대경로면 `remote.path`는 무시된다
|
||||||
|
|
||||||
|
Java `Path.resolve()`는 인자가 절대경로일 때 **기준 경로를 버리고 인자를 그대로 반환**합니다. PPT 가이드의 *"remote.path : 요청시 full path 이면 무시됨"* 이 이 동작입니다.
|
||||||
|
|
||||||
|
| `remote.path` | 요청 `remoteFile` | 최종 원격 경로 | 파일명 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/data/recv` | `test.txt` | `/data/recv/` | `test.txt` |
|
||||||
|
| `/data/recv` | `sub/test.txt` | `/data/recv/sub/` | `test.txt` |
|
||||||
|
| `/data/recv` | `/home/soxor/sftp/svr/test.txt` | `/home/soxor/sftp/svr/` ← **remote.path 무시** | `test.txt` |
|
||||||
|
| `""` (미등록) | `/home/soxor/sftp/svr/test.txt` | `/home/soxor/sftp/svr/` | `test.txt` |
|
||||||
|
|
||||||
|
> 요청자가 절대경로를 보내는 방식이면 `remote.path`는 등록하지 않아도 됩니다. 반대로 `remote.path`로 접근 경로를 **통제하려는 의도라면 이 구조로는 불가능합니다** — 요청자가 절대경로를 보내면 어디든 접근됩니다. 경로 제한이 요건이라면 별도 검증 로직을 추가해야 합니다.
|
||||||
|
|
||||||
|
### 5.2 날짜 토큰 치환
|
||||||
|
|
||||||
|
조합이 끝난 경로에서 `#YYYYMMDD#` 패턴을 찾아 실제 일자로 치환합니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
정규식: #YYYYMMDD([+-]\d+)?#
|
||||||
|
```
|
||||||
|
|
||||||
|
| 표기 | 의미 | 예 (오늘이 2026-09-09) |
|
||||||
|
|---|---|---|
|
||||||
|
| `#YYYYMMDD#` | 당일 | `20260909` |
|
||||||
|
| `#YYYYMMDD-1#` | 전일 | `20260908` |
|
||||||
|
| `#YYYYMMDD-7#` | 7일 전 | `20260902` |
|
||||||
|
| `#YYYYMMDD+1#` | 익일 | `20260910` |
|
||||||
|
|
||||||
|
등록 예:
|
||||||
|
|
||||||
|
```
|
||||||
|
remote.path = /data/recv/#YYYYMMDD# → /data/recv/20260909/
|
||||||
|
remote.path = /data/recv/#YYYYMMDD-1# → /data/recv/20260908/
|
||||||
|
```
|
||||||
|
|
||||||
|
동작상 유의점:
|
||||||
|
|
||||||
|
1. 치환이 발생하면 계산된 일자가 **배치 헤더의 기준일자(BaseDate)로 함께 설정**됩니다. 로그·모니터링의 기준일자가 이 값을 따릅니다.
|
||||||
|
2. 치환 중 예외가 나도 **작업은 중단되지 않고** 어댑터 로그에만 기록됩니다. 이 경우 경로에 `#YYYYMMDD#` 문자열이 그대로 남아 원격 경로 조회 단계에서 실패합니다.
|
||||||
|
3. 경로 끝의 `/`는 없으면 자동으로 붙습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. `protocol` 값 매핑 (주의)
|
||||||
|
|
||||||
|
**가장 오해하기 쉬운 항목입니다.** 매칭은 열거형 이름 기준이고, **매칭에 실패해도 오류 없이 조용히 기본값으로 처리**됩니다.
|
||||||
|
|
||||||
|
| 등록값(대소문자 무관) | 해석 | 내부 코드 | 전송 방식 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ftp` | FTP | `0` | 평문 FTP |
|
||||||
|
| `sftp_passwd` | SFTP + 비밀번호 | `1` | SFTP |
|
||||||
|
| `sftp_key` | SFTP + SSH Key | `2` | SFTP |
|
||||||
|
| **미등록 / 공백 / 그 외 모든 값** | **SFTP + 비밀번호** | `1` | SFTP |
|
||||||
|
|
||||||
|
즉 아래가 모두 **동일하게 `sftp_passwd`로 동작**합니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
protocol = (미등록)
|
||||||
|
protocol = sftp ← 가이드 표기지만 매핑 대상이 아님
|
||||||
|
protocol = SFTP
|
||||||
|
protocol = sftp_password ← 오타
|
||||||
|
```
|
||||||
|
|
||||||
|
지켜야 할 규칙은 두 가지입니다.
|
||||||
|
|
||||||
|
- **FTP를 쓰려면 반드시 `ftp`로 정확히** 등록해야 합니다. 오타 시 SFTP로 접속을 시도해 원인 파악이 어려운 실패가 납니다.
|
||||||
|
- **SSH Key 인증을 쓰려면 반드시 `sftp_key`로** 등록해야 합니다. 비워두거나 `sftp`로 두면 비밀번호 인증으로 동작합니다.
|
||||||
|
|
||||||
|
> 오타가 로그에 남지 않으므로 접속 실패 시 프로퍼티 오타를 우선 의심하십시오. **개선 후보**: 매칭 실패 시 경고 로그를 남기거나 예외를 던지도록 `Protocol.get()`을 수정하는 것이 안전합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. `io.bandwidth` 단위 표기
|
||||||
|
|
||||||
|
문자열을 그대로 파싱하지 않고 **치환 후 정수 변환**합니다.
|
||||||
|
|
||||||
|
```java
|
||||||
|
값.replace("K", "000").replace("M", "000000").replace(",", "")
|
||||||
|
```
|
||||||
|
|
||||||
|
| 등록값 | 실제 적용값 | 설명 |
|
||||||
|
|---|---|---|
|
||||||
|
| `0` 또는 미등록 | `0` | 제한 없음 |
|
||||||
|
| `500K` | `500000` | |
|
||||||
|
| `10M` | `10000000` | |
|
||||||
|
| `1,024` | `1024` | 콤마 제거 |
|
||||||
|
| `500k` | **예외 발생** | 소문자 `k`는 치환되지 않음 → `NumberFormatException` |
|
||||||
|
| `500KB` | **예외 발생** | `B`가 남음 |
|
||||||
|
|
||||||
|
**대문자 `K` / `M`만 사용하십시오.** 표기가 잘못되면 `FtpEnv.init()` 단계에서 예외가 나 다운로드가 시작되지 않습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 파일 저장 위치 프로퍼티 (`ResponseRcvDirInfo`)
|
||||||
|
|
||||||
|
수신 파일이 놓이는 위치는 대외기관 프로퍼티가 아니라 **공통 그룹 `ResponseRcvDirInfo`** 에서 결정됩니다.
|
||||||
|
(EMS 메뉴: 환경정보 → 공통코드관리 → 프라퍼티 관리)
|
||||||
|
|
||||||
|
| 키 | 용도 | FTPDJ에서의 역할 |
|
||||||
|
|---|---|---|
|
||||||
|
| `rcv.real.directory` | 수신 중 임시 디렉터리 | 다운로드가 **먼저 떨어지는 곳** |
|
||||||
|
| `rcv.arch.directory` | 수신 완료 보관 디렉터리 | 완료 후 **이동되는 최종 위치**. check 파일도 여기 |
|
||||||
|
| `rcv.error.directory` | 오류 파일 디렉터리 | 실패 시 받다 만 파일을 이동 |
|
||||||
|
| `rcv.root.directory` | 빈 완료표시 파일 위치 | 0바이트 파일 생성 |
|
||||||
|
| `rcv.root.ext` | check 파일 확장자 | **미등록 시 `.chk`** |
|
||||||
|
| `retension.day.*` | 폴더별 보존일수 | 값(일) 경과 시 자동 삭제. 공백/비정수면 삭제 안 함 |
|
||||||
|
|
||||||
|
### 8.1 경로 조립과 파일 생성 순서
|
||||||
|
|
||||||
|
작업 등록 시 수신 디렉터리는 `rcv.real.directory/업무구분코드/대외기관코드` 로 설정됩니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
① 다운로드 {rcv.real.directory}/{업무구분}/{대외기관}/{파일명}
|
||||||
|
② 이동 {rcv.arch.directory}/{업무구분}/{대외기관}/{파일명}
|
||||||
|
③ check 파일 {rcv.arch.directory}/{업무구분}/{대외기관}/{파일명}{rcv.root.ext}
|
||||||
|
④ 빈 표시 파일 {rcv.root.directory}/{업무구분}/{대외기관}/{파일명}
|
||||||
|
```
|
||||||
|
|
||||||
|
- ①→② 이동은 `REPLACE_EXISTING`이므로 **같은 이름의 기존 파일은 덮어씁니다.**
|
||||||
|
- ③, ④는 생성 전에 기존 파일이 있으면 **먼저 삭제**하고 새로 만듭니다.
|
||||||
|
- 다운로드 중 실패하면 ①의 파일을 `rcv.error.directory`로 옮기고, 이동조차 실패하면 조용히 무시합니다.
|
||||||
|
|
||||||
|
> **순서를 바꾸지 마십시오.** check 파일(③)은 반드시 본 파일 이동(②) **이후**에 생성됩니다. 수신측(BAT)이 check 파일을 폴링하는 구조이므로, check 파일이 보이는 시점에는 본 파일이 이미 최종 위치에 있어야 합니다. 순서를 뒤집으면 BAT가 미완성 파일을 집어갈 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. check 파일 포맷
|
||||||
|
|
||||||
|
`rcv.arch.directory` 아래 `{파일명}.chk`로 생성되며, 기존 EAI(BAT)가 폴링으로 읽어갑니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
%-30s %8s %010d %c %-100s @@
|
||||||
|
```
|
||||||
|
|
||||||
|
| 위치(1-based) | 길이 | 내용 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 – 30 | 30 | InterfaceID (거래구분코드 `bizCode`), 좌측정렬 공백 패딩 |
|
||||||
|
| 31 – 38 | 8 | 기준일자 `YYYYMMDD` (생성 시점의 현재일자) |
|
||||||
|
| 39 – 48 | 10 | 파일 건수, 0 패딩 (**항상 `0000000001`**) |
|
||||||
|
| **49** | 1 | **상태값** — 생성 시 `'0'` 고정 |
|
||||||
|
| 50 – 149 | 100 | 파일명, 좌측정렬 공백 패딩 |
|
||||||
|
| 150 – 151 | 2 | 종료 표시 `@@` |
|
||||||
|
|
||||||
|
총 **151바이트**입니다. 49번째 바이트의 상태값은 콜백 응답 검증에서 다시 쓰입니다(10장). 상수 `BAT_RET_CODE_IDX = 49`가 이 위치를 가리킵니다.
|
||||||
|
|
||||||
|
> **BAT와의 인터페이스 규격입니다.** 포맷을 바꾸면 수신측 파싱이 깨집니다. 변경 시 반드시 BAT 담당자와 합의하십시오.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. `callback.url` 동작
|
||||||
|
|
||||||
|
`callback.url`이 **비어 있으면 콜백 단계 자체를 건너뜁니다.** 등록되어 있으면 다음을 호출합니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET {callback.url}?eaibatMsg={check 파일과 동일한 151바이트 문자열}
|
||||||
|
```
|
||||||
|
|
||||||
|
응답 검증 조건은 세 가지이며, **하나라도 어긋나면 작업 전체가 실패 처리**됩니다.
|
||||||
|
|
||||||
|
| 검증 | 실패 시 메시지 |
|
||||||
|
|---|---|
|
||||||
|
| HTTP 상태코드 `200` | `Response code is not 200.(코드)` |
|
||||||
|
| 응답 본문 길이 ≥ 49바이트 | `BAT return message is too short.` |
|
||||||
|
| 응답 본문 **49번째 바이트 = `'1'`** | `BAT return code is not '1'.(값)` |
|
||||||
|
|
||||||
|
응답 인코딩은 `Content-Type` 헤더의 charset을 따르고, 없으면 UTF-8로 처리합니다.
|
||||||
|
|
||||||
|
> **알아둘 것:** 파일 수신과 저장이 모두 성공해도 콜백 검증에서 실패하면 작업은 오류로 기록됩니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 의존 라이브러리 취약점 조치 내역
|
||||||
|
|
||||||
|
| 취약점 번호 | 해당 jar 파일 | 조치 내용 |
|
||||||
|
|:-------------------|:-----------------------------|:------------------------------------|
|
||||||
|
| CVE-2023-46604<br>CVE-2026-34197 | activemq-broker-5.15.13.jar<br>activemq-client-5.15.13.jar<br>activemq-console-5.15.13.jar<br>activemq-spring-5.15.13.jar | **5.15.13 → 5.19.11** 교체.<br>이 CVE의 최소 수정 버전은 5.15.16이며,<br>현재 활성 라인 최신인 5.19.11을 적용했습니다. |
|
||||||
|
| CVE-2023-48795 | jsch-0.1.55.jar | **com.jcraft:jsch 0.1.55<br>→ com.github.mwiede:jsch 2.28.7** 교체.<br>Terrapin은 0.2.15에서<br>strict KEX 지원으로 수정되었습니다. |
|
||||||
|
| CVE-2023-20863 | spring-core-5.3.27.jar 등<br>spring 계열 14건 | **현재 버전 5.3.27이 바로 이 CVE의 수정 버전입니다.**<br>영향 범위는 5.3.0–5.3.26 및 6.0.0–6.0.7이고,<br>수정 릴리스가 5.3.27 / 6.0.8입니다.<br>\*\* 조치 불필요. 스캔 도구가 영향 범위 상한을<br>잘못 잡은 것으로 보이며, 판정 근거 재확인을 요청드립니다 |
|
||||||
|
| CVE-2026-22740 | spring-core-5.3.27.jar 등<br>spring 계열 14건 | 이 결함은 **WebFlux 서버 애플리케이션 전용**입니다<br>(멀티파트 임시파일 미삭제).<br>본 애플리케이션은 `spring-webmvc` 기반이며<br>`spring-webflux` jar 자체가 배포물에 없습니다. |
|
||||||
Reference in New Issue
Block a user