@simplysm/storage 1.0.135 → 13.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.cache/typecheck-node.tsbuildinfo +1 -0
  2. package/.cache/typecheck-tests-node.tsbuildinfo +1 -0
  3. package/README.md +262 -0
  4. package/dist/clients/ftp-storage-client.js +126 -0
  5. package/dist/clients/ftp-storage-client.js.map +7 -0
  6. package/dist/clients/sftp-storage-client.js +108 -0
  7. package/dist/clients/sftp-storage-client.js.map +7 -0
  8. package/dist/core-common/src/common.types.d.ts +74 -0
  9. package/dist/core-common/src/common.types.d.ts.map +1 -0
  10. package/dist/core-common/src/env.d.ts +6 -0
  11. package/dist/core-common/src/env.d.ts.map +1 -0
  12. package/dist/core-common/src/errors/argument-error.d.ts +25 -0
  13. package/dist/core-common/src/errors/argument-error.d.ts.map +1 -0
  14. package/dist/core-common/src/errors/not-implemented-error.d.ts +29 -0
  15. package/dist/core-common/src/errors/not-implemented-error.d.ts.map +1 -0
  16. package/dist/core-common/src/errors/sd-error.d.ts +27 -0
  17. package/dist/core-common/src/errors/sd-error.d.ts.map +1 -0
  18. package/dist/core-common/src/errors/timeout-error.d.ts +31 -0
  19. package/dist/core-common/src/errors/timeout-error.d.ts.map +1 -0
  20. package/dist/core-common/src/extensions/arr-ext.d.ts +15 -0
  21. package/dist/core-common/src/extensions/arr-ext.d.ts.map +1 -0
  22. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts +19 -0
  23. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts.map +1 -0
  24. package/dist/core-common/src/extensions/arr-ext.types.d.ts +215 -0
  25. package/dist/core-common/src/extensions/arr-ext.types.d.ts.map +1 -0
  26. package/dist/core-common/src/extensions/map-ext.d.ts +57 -0
  27. package/dist/core-common/src/extensions/map-ext.d.ts.map +1 -0
  28. package/dist/core-common/src/extensions/set-ext.d.ts +36 -0
  29. package/dist/core-common/src/extensions/set-ext.d.ts.map +1 -0
  30. package/dist/core-common/src/features/debounce-queue.d.ts +53 -0
  31. package/dist/core-common/src/features/debounce-queue.d.ts.map +1 -0
  32. package/dist/core-common/src/features/event-emitter.d.ts +66 -0
  33. package/dist/core-common/src/features/event-emitter.d.ts.map +1 -0
  34. package/dist/core-common/src/features/serial-queue.d.ts +47 -0
  35. package/dist/core-common/src/features/serial-queue.d.ts.map +1 -0
  36. package/dist/core-common/src/index.d.ts +32 -0
  37. package/dist/core-common/src/index.d.ts.map +1 -0
  38. package/dist/core-common/src/types/date-only.d.ts +152 -0
  39. package/dist/core-common/src/types/date-only.d.ts.map +1 -0
  40. package/dist/core-common/src/types/date-time.d.ts +96 -0
  41. package/dist/core-common/src/types/date-time.d.ts.map +1 -0
  42. package/dist/core-common/src/types/lazy-gc-map.d.ts +80 -0
  43. package/dist/core-common/src/types/lazy-gc-map.d.ts.map +1 -0
  44. package/dist/core-common/src/types/time.d.ts +68 -0
  45. package/dist/core-common/src/types/time.d.ts.map +1 -0
  46. package/dist/core-common/src/types/uuid.d.ts +35 -0
  47. package/dist/core-common/src/types/uuid.d.ts.map +1 -0
  48. package/dist/core-common/src/utils/bytes.d.ts +51 -0
  49. package/dist/core-common/src/utils/bytes.d.ts.map +1 -0
  50. package/dist/core-common/src/utils/date-format.d.ts +90 -0
  51. package/dist/core-common/src/utils/date-format.d.ts.map +1 -0
  52. package/dist/core-common/src/utils/json.d.ts +34 -0
  53. package/dist/core-common/src/utils/json.d.ts.map +1 -0
  54. package/dist/core-common/src/utils/num.d.ts +60 -0
  55. package/dist/core-common/src/utils/num.d.ts.map +1 -0
  56. package/dist/core-common/src/utils/obj.d.ts +258 -0
  57. package/dist/core-common/src/utils/obj.d.ts.map +1 -0
  58. package/dist/core-common/src/utils/path.d.ts +23 -0
  59. package/dist/core-common/src/utils/path.d.ts.map +1 -0
  60. package/dist/core-common/src/utils/primitive.d.ts +18 -0
  61. package/dist/core-common/src/utils/primitive.d.ts.map +1 -0
  62. package/dist/core-common/src/utils/str.d.ts +103 -0
  63. package/dist/core-common/src/utils/str.d.ts.map +1 -0
  64. package/dist/core-common/src/utils/template-strings.d.ts +84 -0
  65. package/dist/core-common/src/utils/template-strings.d.ts.map +1 -0
  66. package/dist/core-common/src/utils/transferable.d.ts +47 -0
  67. package/dist/core-common/src/utils/transferable.d.ts.map +1 -0
  68. package/dist/core-common/src/utils/wait.d.ts +19 -0
  69. package/dist/core-common/src/utils/wait.d.ts.map +1 -0
  70. package/dist/core-common/src/utils/xml.d.ts +36 -0
  71. package/dist/core-common/src/utils/xml.d.ts.map +1 -0
  72. package/dist/core-common/src/zip/sd-zip.d.ts +80 -0
  73. package/dist/core-common/src/zip/sd-zip.d.ts.map +1 -0
  74. package/dist/index.js +7 -10
  75. package/dist/index.js.map +7 -1
  76. package/dist/storage/src/clients/ftp-storage-client.d.ts +56 -0
  77. package/dist/storage/src/clients/ftp-storage-client.d.ts.map +1 -0
  78. package/dist/storage/src/clients/sftp-storage-client.d.ts +48 -0
  79. package/dist/storage/src/clients/sftp-storage-client.d.ts.map +1 -0
  80. package/dist/storage/src/index.d.ts +7 -0
  81. package/dist/storage/src/index.d.ts.map +1 -0
  82. package/dist/storage/src/storage-factory.d.ts +20 -0
  83. package/dist/storage/src/storage-factory.d.ts.map +1 -0
  84. package/dist/storage/src/types/storage-conn-config.d.ts +7 -0
  85. package/dist/storage/src/types/storage-conn-config.d.ts.map +1 -0
  86. package/dist/storage/src/types/storage-type.d.ts +2 -0
  87. package/dist/storage/src/types/storage-type.d.ts.map +1 -0
  88. package/dist/storage/src/types/storage.d.ts +19 -0
  89. package/dist/storage/src/types/storage.d.ts.map +1 -0
  90. package/dist/storage-factory.js +35 -0
  91. package/dist/storage-factory.js.map +7 -0
  92. package/dist/types/storage-conn-config.js +1 -0
  93. package/dist/types/storage-conn-config.js.map +7 -0
  94. package/dist/types/storage-type.js +1 -0
  95. package/dist/types/storage-type.js.map +7 -0
  96. package/dist/types/storage.js +1 -0
  97. package/dist/types/storage.js.map +7 -0
  98. package/package.json +24 -1
  99. package/src/clients/ftp-storage-client.ts +146 -0
  100. package/src/clients/sftp-storage-client.ts +135 -0
  101. package/src/index.ts +14 -4
  102. package/src/storage-factory.ts +47 -0
  103. package/src/types/storage-conn-config.ts +6 -0
  104. package/src/types/storage-type.ts +1 -0
  105. package/src/types/storage.ts +20 -0
  106. package/tests/ftp-storage-client.spec.ts +259 -0
  107. package/tests/sftp-storage-client.spec.ts +251 -0
  108. package/tests/storage-factory.spec.ts +160 -0
  109. package/dist/common/IStorage.d.ts +0 -7
  110. package/dist/common/IStorage.js +0 -3
  111. package/dist/common/IStorage.js.map +0 -1
  112. package/dist/ftp/FtpStorage.d.ts +0 -11
  113. package/dist/ftp/FtpStorage.js +0 -165
  114. package/dist/ftp/FtpStorage.js.map +0 -1
  115. package/dist/ftp/IFtpConnectionConfig.d.ts +0 -6
  116. package/dist/ftp/IFtpConnectionConfig.js +0 -3
  117. package/dist/ftp/IFtpConnectionConfig.js.map +0 -1
  118. package/dist/index.d.ts +0 -4
  119. package/src/common/IStorage.ts +0 -9
  120. package/src/ftp/FtpStorage.ts +0 -87
  121. package/src/ftp/IFtpConnectionConfig.ts +0 -6
  122. package/tsconfig.build.json +0 -17
  123. package/tsconfig.json +0 -17
  124. package/tslint.json +0 -3
package/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # @simplysm/storage
2
+
3
+ A storage client package that supports FTP, FTPS, and SFTP protocols. Through the unified `Storage` interface, you can perform file upload, download, directory manipulation and other operations with the same API regardless of protocol.
4
+
5
+ Using `StorageFactory`, you can automatically manage connection/disconnection, and you can also directly instantiate `FtpStorageClient` or `SftpStorageClient` if needed.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @simplysm/storage
11
+ # or
12
+ pnpm add @simplysm/storage
13
+ ```
14
+
15
+ ### Dependencies
16
+
17
+ | Package | Description |
18
+ |--------|------|
19
+ | `@simplysm/core-common` | Common utilities (`Bytes` type, etc.) |
20
+ | `basic-ftp` | FTP/FTPS protocol implementation |
21
+ | `ssh2-sftp-client` | SFTP protocol implementation |
22
+
23
+ ## Core Modules
24
+
25
+ ### Export List
26
+
27
+ | Module | Type | Description |
28
+ |------|------|------|
29
+ | `StorageFactory` | Class | Creates clients based on storage type and automatically manages connection/disconnection |
30
+ | `FtpStorageClient` | Class | FTP/FTPS protocol client (based on `basic-ftp`) |
31
+ | `SftpStorageClient` | Class | SFTP protocol client (based on `ssh2-sftp-client`) |
32
+ | `Storage` | Interface | Common interface implemented by all storage clients |
33
+ | `StorageConnConfig` | Interface | Connection configuration |
34
+ | `FileInfo` | Interface | Directory entry information |
35
+ | `StorageType` | Type | Storage protocol types (`"ftp" \| "ftps" \| "sftp"`) |
36
+
37
+ ## Type Definitions
38
+
39
+ ### StorageConnConfig
40
+
41
+ Configuration required for server connection.
42
+
43
+ ```typescript
44
+ interface StorageConnConfig {
45
+ host: string; // Server host
46
+ port?: number; // Port (FTP default: 21, SFTP default: 22)
47
+ user?: string; // Username
48
+ pass?: string; // Password
49
+ }
50
+ ```
51
+
52
+ ### FileInfo
53
+
54
+ File/directory information returned by `readdir()`.
55
+
56
+ ```typescript
57
+ interface FileInfo {
58
+ name: string; // File or directory name
59
+ isFile: boolean; // true if file, false if directory
60
+ }
61
+ ```
62
+
63
+ ### StorageType
64
+
65
+ Supported storage protocol types.
66
+
67
+ ```typescript
68
+ type StorageType = "ftp" | "ftps" | "sftp";
69
+ ```
70
+
71
+ | Value | Protocol | Default Port | Description |
72
+ |-----|---------|----------|------|
73
+ | `"ftp"` | FTP | 21 | Unencrypted FTP |
74
+ | `"ftps"` | FTPS | 21 | TLS-encrypted FTP |
75
+ | `"sftp"` | SFTP | 22 | SSH-based file transfer |
76
+
77
+ ### Storage Interface
78
+
79
+ Common interface implemented by all storage clients (`FtpStorageClient`, `SftpStorageClient`). `Bytes` is a `Uint8Array` type alias defined in `@simplysm/core-common`.
80
+
81
+ | Method | Signature | Description |
82
+ |--------|---------|------|
83
+ | `connect` | `(config: StorageConnConfig) => Promise<void>` | Connect to server |
84
+ | `close` | `() => Promise<void>` | Close connection |
85
+ | `put` | `(localPathOrBuffer: string \| Bytes, storageFilePath: string) => Promise<void>` | Upload file (local path or byte data) |
86
+ | `readFile` | `(filePath: string) => Promise<Bytes>` | Download file (returns `Bytes`) |
87
+ | `readdir` | `(dirPath: string) => Promise<FileInfo[]>` | List directory contents |
88
+ | `remove` | `(filePath: string) => Promise<void>` | Delete file |
89
+ | `exists` | `(filePath: string) => Promise<boolean>` | Check if file/directory exists |
90
+ | `mkdir` | `(dirPath: string) => Promise<void>` | Create directory (recursive) |
91
+ | `rename` | `(fromPath: string, toPath: string) => Promise<void>` | Rename file/directory |
92
+ | `uploadDir` | `(fromPath: string, toPath: string) => Promise<void>` | Upload entire local directory to remote |
93
+
94
+ ## Usage
95
+
96
+ ### StorageFactory (Recommended)
97
+
98
+ `StorageFactory.connect()` automatically manages connection and disconnection with a callback pattern. The connection is always closed even if an exception occurs in the callback, so it's recommended over using clients directly.
99
+
100
+ ```typescript
101
+ import { StorageFactory } from "@simplysm/storage";
102
+
103
+ // FTP connection
104
+ const result = await StorageFactory.connect("ftp", {
105
+ host: "ftp.example.com",
106
+ port: 21,
107
+ user: "username",
108
+ pass: "password",
109
+ }, async (client) => {
110
+ // Upload local file to remote server
111
+ await client.put("/local/path/file.txt", "/remote/path/file.txt");
112
+
113
+ // Upload byte data directly
114
+ const data = new TextEncoder().encode("hello world");
115
+ await client.put(data, "/remote/path/hello.txt");
116
+
117
+ // Download remote file
118
+ const content = await client.readFile("/remote/path/file.txt");
119
+
120
+ // The callback's return value becomes the return value of StorageFactory.connect()
121
+ return content;
122
+ });
123
+ ```
124
+
125
+ ```typescript
126
+ // FTPS connection (TLS encryption)
127
+ await StorageFactory.connect("ftps", {
128
+ host: "ftps.example.com",
129
+ user: "username",
130
+ pass: "password",
131
+ }, async (client) => {
132
+ await client.put("/local/file.txt", "/remote/file.txt");
133
+ });
134
+ ```
135
+
136
+ ```typescript
137
+ // SFTP connection
138
+ await StorageFactory.connect("sftp", {
139
+ host: "sftp.example.com",
140
+ port: 22,
141
+ user: "username",
142
+ pass: "password",
143
+ }, async (client) => {
144
+ // List directory contents
145
+ const files = await client.readdir("/remote/path");
146
+ for (const file of files) {
147
+ console.log(`${file.name} - ${file.isFile ? "File" : "Directory"}`);
148
+ }
149
+
150
+ // Upload entire directory
151
+ await client.uploadDir("/local/dir", "/remote/dir");
152
+ });
153
+ ```
154
+
155
+ ### FtpStorageClient (Direct Usage)
156
+
157
+ Client that uses FTP or FTPS protocol. The `secure` parameter in the constructor determines whether to use FTPS.
158
+
159
+ ```typescript
160
+ import { FtpStorageClient } from "@simplysm/storage";
161
+
162
+ // FTP client (secure: false is default)
163
+ const client = new FtpStorageClient();
164
+
165
+ // FTPS client
166
+ const secureClient = new FtpStorageClient(true);
167
+
168
+ await client.connect({
169
+ host: "ftp.example.com",
170
+ port: 21,
171
+ user: "username",
172
+ pass: "password",
173
+ });
174
+
175
+ try {
176
+ // Upload file - from local file path
177
+ await client.put("/local/path/file.txt", "/remote/path/file.txt");
178
+
179
+ // Upload file - from Uint8Array byte data
180
+ const bytes = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
181
+ await client.put(bytes, "/remote/path/hello.bin");
182
+
183
+ // Download file (returns Bytes, i.e. Uint8Array)
184
+ const data = await client.readFile("/remote/path/file.txt");
185
+ const text = new TextDecoder().decode(data);
186
+
187
+ // List directory contents
188
+ const files = await client.readdir("/remote/path");
189
+
190
+ // Check if file/directory exists
191
+ const exists = await client.exists("/remote/path/file.txt");
192
+
193
+ // Create directory (creates parent directories too)
194
+ await client.mkdir("/remote/new/nested/path");
195
+
196
+ // Rename file
197
+ await client.rename("/remote/old-name.txt", "/remote/new-name.txt");
198
+
199
+ // Delete file
200
+ await client.remove("/remote/path/file.txt");
201
+
202
+ // Upload entire local directory to remote
203
+ await client.uploadDir("/local/dir", "/remote/dir");
204
+ } finally {
205
+ // Connection must be closed
206
+ await client.close();
207
+ }
208
+ ```
209
+
210
+ ### SftpStorageClient (Direct Usage)
211
+
212
+ Client that uses SFTP protocol. It implements the same `Storage` interface as `FtpStorageClient`, so the API is identical.
213
+
214
+ ```typescript
215
+ import { SftpStorageClient } from "@simplysm/storage";
216
+
217
+ const client = new SftpStorageClient();
218
+
219
+ await client.connect({
220
+ host: "sftp.example.com",
221
+ port: 22,
222
+ user: "username",
223
+ pass: "password",
224
+ });
225
+
226
+ try {
227
+ // All methods of the Storage interface can be used identically
228
+ await client.put("/local/path/file.txt", "/remote/path/file.txt");
229
+ const data = await client.readFile("/remote/path/file.txt");
230
+ const files = await client.readdir("/remote/path");
231
+ const exists = await client.exists("/remote/path/file.txt");
232
+ await client.mkdir("/remote/new/path");
233
+ await client.rename("/remote/old.txt", "/remote/new.txt");
234
+ await client.remove("/remote/path/file.txt");
235
+ await client.uploadDir("/local/dir", "/remote/dir");
236
+ } finally {
237
+ await client.close();
238
+ }
239
+ ```
240
+
241
+ ## Important Notes
242
+
243
+ ### Connection Management
244
+
245
+ - Using `StorageFactory.connect()` is recommended. The connection is automatically closed when the callback ends, and closure is guaranteed in the `finally` block even if an exception occurs.
246
+ - When using clients directly, you must call `close()` with a `try/finally` pattern. Otherwise, connections may leak.
247
+ - Calling `connect()` again on an already connected instance will cause an error. If reconnection is needed, call `close()` first.
248
+ - Calling `close()` when already closed does not cause an error.
249
+
250
+ ### exists() Behavior
251
+
252
+ - FTP: Checks files with the `SIZE` command (O(1)), and on failure, lists the parent directory to check if a directory exists. Performance may degrade in directories with many entries.
253
+ - SFTP: Uses `ssh2-sftp-client`'s `exists()` method, returns `true` for files (`"-"`), directories (`"d"`), and symbolic links (`"l"`).
254
+ - Both implementations return `false` instead of throwing exceptions when the parent directory doesn't exist or on network/permission errors.
255
+
256
+ ### Byte Data Type
257
+
258
+ - `Bytes` used in the return type of `readFile()` and input type of `put()` is a `Uint8Array` type alias defined in `@simplysm/core-common`.
259
+
260
+ ## License
261
+
262
+ Apache-2.0
@@ -0,0 +1,126 @@
1
+ import { bytesConcat, SdError } from "@simplysm/core-common";
2
+ import ftp from "basic-ftp";
3
+ import { PassThrough, Readable } from "stream";
4
+ class FtpStorageClient {
5
+ constructor(_secure = false) {
6
+ this._secure = _secure;
7
+ }
8
+ _client;
9
+ /**
10
+ * FTP 서버에 연결합니다.
11
+ *
12
+ * @remarks
13
+ * - 연결 후 반드시 {@link close}로 연결을 종료해야 합니다.
14
+ * - 동일 인스턴스에서 여러 번 호출하지 마세요. (연결 누수 발생)
15
+ * - 자동 연결/종료 관리가 필요하면 {@link StorageFactory.connect}를 사용하세요. (권장)
16
+ */
17
+ async connect(config) {
18
+ if (this._client !== void 0) {
19
+ throw new SdError("\uC774\uBBF8 FTP \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 close()\uB97C \uD638\uCD9C\uD558\uC138\uC694.");
20
+ }
21
+ const client = new ftp.Client();
22
+ try {
23
+ await client.access({
24
+ host: config.host,
25
+ port: config.port,
26
+ user: config.user,
27
+ password: config.pass,
28
+ secure: this._secure
29
+ });
30
+ this._client = client;
31
+ } catch (err) {
32
+ client.close();
33
+ throw err;
34
+ }
35
+ }
36
+ _requireClient() {
37
+ if (this._client === void 0) {
38
+ throw new SdError("FTP \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uB418\uC5B4\uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
39
+ }
40
+ return this._client;
41
+ }
42
+ /** 디렉토리를 생성합니다. 상위 디렉토리가 없으면 함께 생성합니다. */
43
+ async mkdir(dirPath) {
44
+ await this._requireClient().ensureDir(dirPath);
45
+ }
46
+ async rename(fromPath, toPath) {
47
+ await this._requireClient().rename(fromPath, toPath);
48
+ }
49
+ async readdir(dirPath) {
50
+ const fileInfos = await this._requireClient().list(dirPath);
51
+ return fileInfos.map((item) => ({ name: item.name, isFile: item.isFile }));
52
+ }
53
+ async readFile(filePath) {
54
+ const client = this._requireClient();
55
+ const chunks = [];
56
+ const writable = new PassThrough();
57
+ writable.on("data", (chunk) => {
58
+ chunks.push(chunk);
59
+ });
60
+ await client.downloadTo(writable, filePath);
61
+ return bytesConcat(chunks);
62
+ }
63
+ /**
64
+ * 파일 또는 디렉토리 존재 여부를 확인합니다.
65
+ *
66
+ * @remarks
67
+ * 파일 확인 시 size() 명령으로 O(1) 성능을 제공합니다.
68
+ * 디렉토리 확인 시 상위 디렉토리 목록을 조회하므로, 항목 수가 많으면 성능이 저하될 수 있습니다.
69
+ *
70
+ * 슬래시가 없는 경로(예: `file.txt`)는 루트 디렉토리(`/`)에서 검색합니다.
71
+ *
72
+ * 상위 디렉토리가 존재하지 않는 경우에도 false를 반환합니다.
73
+ * 네트워크 오류, 권한 오류 등 모든 예외도 false를 반환합니다.
74
+ */
75
+ async exists(filePath) {
76
+ try {
77
+ await this._requireClient().size(filePath);
78
+ return true;
79
+ } catch {
80
+ try {
81
+ const lastSlash = filePath.lastIndexOf("/");
82
+ const dirPath = lastSlash > 0 ? filePath.substring(0, lastSlash) : "/";
83
+ const fileName = filePath.substring(lastSlash + 1);
84
+ const list = await this._requireClient().list(dirPath);
85
+ return list.some((item) => item.name === fileName);
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+ }
91
+ async remove(filePath) {
92
+ await this._requireClient().remove(filePath);
93
+ }
94
+ /** 로컬 파일 경로 또는 바이트 데이터를 원격 경로에 업로드합니다. */
95
+ async put(localPathOrBuffer, storageFilePath) {
96
+ let param;
97
+ if (typeof localPathOrBuffer === "string") {
98
+ param = localPathOrBuffer;
99
+ } else {
100
+ param = Readable.from(localPathOrBuffer);
101
+ }
102
+ await this._requireClient().uploadFrom(param, storageFilePath);
103
+ }
104
+ async uploadDir(fromPath, toPath) {
105
+ await this._requireClient().uploadFromDir(fromPath, toPath);
106
+ }
107
+ /**
108
+ * 연결을 종료합니다.
109
+ *
110
+ * @remarks
111
+ * 이미 종료된 상태에서 호출해도 에러가 발생하지 않습니다.
112
+ * 종료 후에는 동일 인스턴스에서 {@link connect}를 다시 호출하여 재연결할 수 있습니다.
113
+ */
114
+ close() {
115
+ if (this._client === void 0) {
116
+ return Promise.resolve();
117
+ }
118
+ this._client.close();
119
+ this._client = void 0;
120
+ return Promise.resolve();
121
+ }
122
+ }
123
+ export {
124
+ FtpStorageClient
125
+ };
126
+ //# sourceMappingURL=ftp-storage-client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/clients/ftp-storage-client.ts"],
4
+ "sourcesContent": ["import type { Bytes } from \"@simplysm/core-common\";\nimport { bytesConcat, SdError } from \"@simplysm/core-common\";\nimport ftp from \"basic-ftp\";\nimport { PassThrough, Readable } from \"stream\";\nimport type { Storage, FileInfo } from \"../types/storage\";\nimport type { StorageConnConfig } from \"../types/storage-conn-config\";\n\n/**\n * FTP/FTPS \uD504\uB85C\uD1A0\uCF5C\uC744 \uC0AC\uC6A9\uD558\uB294 \uC2A4\uD1A0\uB9AC\uC9C0 \uD074\uB77C\uC774\uC5B8\uD2B8.\n *\n * @remarks\n * \uC0DD\uC131\uC790\uC758 `secure` \uD30C\uB77C\uBBF8\uD130\uB85C FTPS \uC0AC\uC6A9 \uC5EC\uBD80\uB97C \uC124\uC815\uD569\uB2C8\uB2E4.\n * \uC9C1\uC811 \uC0AC\uC6A9\uBCF4\uB2E4 {@link StorageFactory.connect}\uB97C \uD1B5\uD55C \uC0AC\uC6A9\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n */\nexport class FtpStorageClient implements Storage {\n private _client: ftp.Client | undefined;\n\n constructor(private readonly _secure: boolean = false) {}\n\n /**\n * FTP \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD569\uB2C8\uB2E4.\n *\n * @remarks\n * - \uC5F0\uACB0 \uD6C4 \uBC18\uB4DC\uC2DC {@link close}\uB85C \uC5F0\uACB0\uC744 \uC885\uB8CC\uD574\uC57C \uD569\uB2C8\uB2E4.\n * - \uB3D9\uC77C \uC778\uC2A4\uD134\uC2A4\uC5D0\uC11C \uC5EC\uB7EC \uBC88 \uD638\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694. (\uC5F0\uACB0 \uB204\uC218 \uBC1C\uC0DD)\n * - \uC790\uB3D9 \uC5F0\uACB0/\uC885\uB8CC \uAD00\uB9AC\uAC00 \uD544\uC694\uD558\uBA74 {@link StorageFactory.connect}\uB97C \uC0AC\uC6A9\uD558\uC138\uC694. (\uAD8C\uC7A5)\n */\n async connect(config: StorageConnConfig): Promise<void> {\n if (this._client !== undefined) {\n throw new SdError(\"\uC774\uBBF8 FTP \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 close()\uB97C \uD638\uCD9C\uD558\uC138\uC694.\");\n }\n const client = new ftp.Client();\n try {\n await client.access({\n host: config.host,\n port: config.port,\n user: config.user,\n password: config.pass,\n secure: this._secure,\n });\n this._client = client;\n } catch (err) {\n client.close();\n throw err;\n }\n }\n\n private _requireClient(): ftp.Client {\n if (this._client === undefined) {\n throw new SdError(\"FTP \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uB418\uC5B4\uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.\");\n }\n return this._client;\n }\n\n /** \uB514\uB809\uD1A0\uB9AC\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4. \uC0C1\uC704 \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC73C\uBA74 \uD568\uAED8 \uC0DD\uC131\uD569\uB2C8\uB2E4. */\n async mkdir(dirPath: string): Promise<void> {\n await this._requireClient().ensureDir(dirPath);\n }\n\n async rename(fromPath: string, toPath: string): Promise<void> {\n await this._requireClient().rename(fromPath, toPath);\n }\n\n async readdir(dirPath: string): Promise<FileInfo[]> {\n const fileInfos = await this._requireClient().list(dirPath);\n return fileInfos.map((item) => ({ name: item.name, isFile: item.isFile }));\n }\n\n async readFile(filePath: string): Promise<Bytes> {\n const client = this._requireClient();\n const chunks: Bytes[] = [];\n const writable = new PassThrough();\n writable.on(\"data\", (chunk: Uint8Array) => {\n chunks.push(chunk);\n });\n await client.downloadTo(writable, filePath);\n return bytesConcat(chunks);\n }\n\n /**\n * \uD30C\uC77C \uB610\uB294 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC \uC5EC\uBD80\uB97C \uD655\uC778\uD569\uB2C8\uB2E4.\n *\n * @remarks\n * \uD30C\uC77C \uD655\uC778 \uC2DC size() \uBA85\uB839\uC73C\uB85C O(1) \uC131\uB2A5\uC744 \uC81C\uACF5\uD569\uB2C8\uB2E4.\n * \uB514\uB809\uD1A0\uB9AC \uD655\uC778 \uC2DC \uC0C1\uC704 \uB514\uB809\uD1A0\uB9AC \uBAA9\uB85D\uC744 \uC870\uD68C\uD558\uBBC0\uB85C, \uD56D\uBAA9 \uC218\uAC00 \uB9CE\uC73C\uBA74 \uC131\uB2A5\uC774 \uC800\uD558\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n *\n * \uC2AC\uB798\uC2DC\uAC00 \uC5C6\uB294 \uACBD\uB85C(\uC608: `file.txt`)\uB294 \uB8E8\uD2B8 \uB514\uB809\uD1A0\uB9AC(`/`)\uC5D0\uC11C \uAC80\uC0C9\uD569\uB2C8\uB2E4.\n *\n * \uC0C1\uC704 \uB514\uB809\uD1A0\uB9AC\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uB294 \uACBD\uC6B0\uC5D0\uB3C4 false\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4.\n * \uB124\uD2B8\uC6CC\uD06C \uC624\uB958, \uAD8C\uD55C \uC624\uB958 \uB4F1 \uBAA8\uB4E0 \uC608\uC678\uB3C4 false\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4.\n */\n async exists(filePath: string): Promise<boolean> {\n try {\n // \uD30C\uC77C\uC778 \uACBD\uC6B0 size()\uB85C \uBE60\uB974\uAC8C \uD655\uC778 (O(1))\n await this._requireClient().size(filePath);\n return true;\n } catch {\n // size() \uC2E4\uD328 \uC2DC \uB514\uB809\uD1A0\uB9AC\uC77C \uC218 \uC788\uC73C\uBBC0\uB85C list()\uB85C \uD655\uC778\n try {\n const lastSlash = filePath.lastIndexOf(\"/\");\n const dirPath = lastSlash > 0 ? filePath.substring(0, lastSlash) : \"/\";\n const fileName = filePath.substring(lastSlash + 1);\n const list = await this._requireClient().list(dirPath);\n return list.some((item) => item.name === fileName);\n } catch {\n return false;\n }\n }\n }\n\n async remove(filePath: string): Promise<void> {\n await this._requireClient().remove(filePath);\n }\n\n /** \uB85C\uCEEC \uD30C\uC77C \uACBD\uB85C \uB610\uB294 \uBC14\uC774\uD2B8 \uB370\uC774\uD130\uB97C \uC6D0\uACA9 \uACBD\uB85C\uC5D0 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4. */\n async put(localPathOrBuffer: string | Bytes, storageFilePath: string): Promise<void> {\n let param: string | Readable;\n if (typeof localPathOrBuffer === \"string\") {\n param = localPathOrBuffer;\n } else {\n param = Readable.from(localPathOrBuffer);\n }\n await this._requireClient().uploadFrom(param, storageFilePath);\n }\n\n async uploadDir(fromPath: string, toPath: string): Promise<void> {\n await this._requireClient().uploadFromDir(fromPath, toPath);\n }\n\n /**\n * \uC5F0\uACB0\uC744 \uC885\uB8CC\uD569\uB2C8\uB2E4.\n *\n * @remarks\n * \uC774\uBBF8 \uC885\uB8CC\uB41C \uC0C1\uD0DC\uC5D0\uC11C \uD638\uCD9C\uD574\uB3C4 \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.\n * \uC885\uB8CC \uD6C4\uC5D0\uB294 \uB3D9\uC77C \uC778\uC2A4\uD134\uC2A4\uC5D0\uC11C {@link connect}\uB97C \uB2E4\uC2DC \uD638\uCD9C\uD558\uC5EC \uC7AC\uC5F0\uACB0\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n */\n close(): Promise<void> {\n if (this._client === undefined) {\n return Promise.resolve();\n }\n\n this._client.close();\n this._client = undefined;\n return Promise.resolve();\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,108 @@
1
+ import { SdError } from "@simplysm/core-common";
2
+ import SftpClient from "ssh2-sftp-client";
3
+ class SftpStorageClient {
4
+ _client;
5
+ /**
6
+ * SFTP 서버에 연결합니다.
7
+ *
8
+ * @remarks
9
+ * - 연결 후 반드시 {@link close}로 연결을 종료해야 합니다.
10
+ * - 동일 인스턴스에서 여러 번 호출하지 마세요. (연결 누수 발생)
11
+ * - 자동 연결/종료 관리가 필요하면 {@link StorageFactory.connect}를 사용하세요. (권장)
12
+ */
13
+ async connect(config) {
14
+ if (this._client !== void 0) {
15
+ throw new SdError("\uC774\uBBF8 SFTP \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 close()\uB97C \uD638\uCD9C\uD558\uC138\uC694.");
16
+ }
17
+ const client = new SftpClient();
18
+ try {
19
+ await client.connect({
20
+ host: config.host,
21
+ port: config.port,
22
+ username: config.user,
23
+ password: config.pass
24
+ });
25
+ this._client = client;
26
+ } catch (err) {
27
+ await client.end();
28
+ throw err;
29
+ }
30
+ }
31
+ _requireClient() {
32
+ if (this._client === void 0) {
33
+ throw new SdError("SFTP \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uB418\uC5B4\uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.");
34
+ }
35
+ return this._client;
36
+ }
37
+ /** 디렉토리를 생성합니다. 상위 디렉토리가 없으면 함께 생성합니다. */
38
+ async mkdir(dirPath) {
39
+ await this._requireClient().mkdir(dirPath, true);
40
+ }
41
+ async rename(fromPath, toPath) {
42
+ await this._requireClient().rename(fromPath, toPath);
43
+ }
44
+ /**
45
+ * 파일 또는 디렉토리 존재 여부를 확인합니다.
46
+ *
47
+ * @remarks
48
+ * 상위 디렉토리가 존재하지 않는 경우에도 false를 반환합니다.
49
+ * 네트워크 오류, 권한 오류 등 모든 예외도 false를 반환합니다.
50
+ */
51
+ async exists(filePath) {
52
+ try {
53
+ const result = await this._requireClient().exists(filePath);
54
+ return typeof result === "string";
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+ async readdir(dirPath) {
60
+ const list = await this._requireClient().list(dirPath);
61
+ return list.map((item) => ({
62
+ name: item.name,
63
+ isFile: item.type === "-"
64
+ }));
65
+ }
66
+ async readFile(filePath) {
67
+ const result = await this._requireClient().get(filePath);
68
+ if (result instanceof Uint8Array) {
69
+ return result;
70
+ }
71
+ if (typeof result === "string") {
72
+ return new TextEncoder().encode(result);
73
+ }
74
+ throw new SdError("\uC608\uC0C1\uCE58 \uBABB\uD55C \uC751\uB2F5 \uD0C0\uC785\uC785\uB2C8\uB2E4.");
75
+ }
76
+ async remove(filePath) {
77
+ await this._requireClient().delete(filePath);
78
+ }
79
+ /** 로컬 파일 경로 또는 바이트 데이터를 원격 경로에 업로드합니다. */
80
+ async put(localPathOrBuffer, storageFilePath) {
81
+ if (typeof localPathOrBuffer === "string") {
82
+ await this._requireClient().fastPut(localPathOrBuffer, storageFilePath);
83
+ } else {
84
+ await this._requireClient().put(Buffer.from(localPathOrBuffer), storageFilePath);
85
+ }
86
+ }
87
+ async uploadDir(fromPath, toPath) {
88
+ await this._requireClient().uploadDir(fromPath, toPath);
89
+ }
90
+ /**
91
+ * 연결을 종료합니다.
92
+ *
93
+ * @remarks
94
+ * 이미 종료된 상태에서 호출해도 에러가 발생하지 않습니다.
95
+ * 종료 후에는 동일 인스턴스에서 {@link connect}를 다시 호출하여 재연결할 수 있습니다.
96
+ */
97
+ async close() {
98
+ if (this._client === void 0) {
99
+ return;
100
+ }
101
+ await this._client.end();
102
+ this._client = void 0;
103
+ }
104
+ }
105
+ export {
106
+ SftpStorageClient
107
+ };
108
+ //# sourceMappingURL=sftp-storage-client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/clients/sftp-storage-client.ts"],
4
+ "sourcesContent": ["import type { Bytes } from \"@simplysm/core-common\";\nimport { SdError } from \"@simplysm/core-common\";\nimport SftpClient from \"ssh2-sftp-client\";\nimport type { Storage, FileInfo } from \"../types/storage\";\nimport type { StorageConnConfig } from \"../types/storage-conn-config\";\n\n// ssh2-sftp-client \uB77C\uC774\uBE0C\uB7EC\uB9AC \uD0C0\uC785 \uC815\uC758\uC5D0\uC11C Buffer \uC0AC\uC6A9\ntype SftpGetResult = string | NodeJS.WritableStream | Bytes;\n\n/**\n * SFTP \uD504\uB85C\uD1A0\uCF5C\uC744 \uC0AC\uC6A9\uD558\uB294 \uC2A4\uD1A0\uB9AC\uC9C0 \uD074\uB77C\uC774\uC5B8\uD2B8.\n *\n * @remarks\n * \uC9C1\uC811 \uC0AC\uC6A9\uBCF4\uB2E4 {@link StorageFactory.connect}\uB97C \uD1B5\uD55C \uC0AC\uC6A9\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n */\nexport class SftpStorageClient implements Storage {\n private _client: SftpClient | undefined;\n\n /**\n * SFTP \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uD569\uB2C8\uB2E4.\n *\n * @remarks\n * - \uC5F0\uACB0 \uD6C4 \uBC18\uB4DC\uC2DC {@link close}\uB85C \uC5F0\uACB0\uC744 \uC885\uB8CC\uD574\uC57C \uD569\uB2C8\uB2E4.\n * - \uB3D9\uC77C \uC778\uC2A4\uD134\uC2A4\uC5D0\uC11C \uC5EC\uB7EC \uBC88 \uD638\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694. (\uC5F0\uACB0 \uB204\uC218 \uBC1C\uC0DD)\n * - \uC790\uB3D9 \uC5F0\uACB0/\uC885\uB8CC \uAD00\uB9AC\uAC00 \uD544\uC694\uD558\uBA74 {@link StorageFactory.connect}\uB97C \uC0AC\uC6A9\uD558\uC138\uC694. (\uAD8C\uC7A5)\n */\n async connect(config: StorageConnConfig): Promise<void> {\n if (this._client !== undefined) {\n throw new SdError(\"\uC774\uBBF8 SFTP \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 close()\uB97C \uD638\uCD9C\uD558\uC138\uC694.\");\n }\n\n const client = new SftpClient();\n try {\n await client.connect({\n host: config.host,\n port: config.port,\n username: config.user,\n password: config.pass,\n });\n this._client = client;\n } catch (err) {\n await client.end();\n throw err;\n }\n }\n\n private _requireClient(): SftpClient {\n if (this._client === undefined) {\n throw new SdError(\"SFTP \uC11C\uBC84\uC5D0 \uC5F0\uACB0\uB418\uC5B4\uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.\");\n }\n return this._client;\n }\n\n /** \uB514\uB809\uD1A0\uB9AC\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4. \uC0C1\uC704 \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC73C\uBA74 \uD568\uAED8 \uC0DD\uC131\uD569\uB2C8\uB2E4. */\n async mkdir(dirPath: string): Promise<void> {\n await this._requireClient().mkdir(dirPath, true);\n }\n\n async rename(fromPath: string, toPath: string): Promise<void> {\n await this._requireClient().rename(fromPath, toPath);\n }\n\n /**\n * \uD30C\uC77C \uB610\uB294 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC \uC5EC\uBD80\uB97C \uD655\uC778\uD569\uB2C8\uB2E4.\n *\n * @remarks\n * \uC0C1\uC704 \uB514\uB809\uD1A0\uB9AC\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uB294 \uACBD\uC6B0\uC5D0\uB3C4 false\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4.\n * \uB124\uD2B8\uC6CC\uD06C \uC624\uB958, \uAD8C\uD55C \uC624\uB958 \uB4F1 \uBAA8\uB4E0 \uC608\uC678\uB3C4 false\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4.\n */\n async exists(filePath: string): Promise<boolean> {\n try {\n // ssh2-sftp-client\uC758 exists()\uB294 false | 'd' | '-' | 'l' \uB97C \uBC18\uD658\uD55C\uB2E4.\n // false: \uC874\uC7AC\uD558\uC9C0 \uC54A\uC74C, 'd': \uB514\uB809\uD1A0\uB9AC, '-': \uD30C\uC77C, 'l': \uC2EC\uBCFC\uB9AD \uB9C1\uD06C\n const result = await this._requireClient().exists(filePath);\n return typeof result === \"string\";\n } catch {\n return false;\n }\n }\n\n async readdir(dirPath: string): Promise<FileInfo[]> {\n const list = await this._requireClient().list(dirPath);\n return list.map((item) => ({\n name: item.name,\n isFile: item.type === \"-\",\n }));\n }\n\n async readFile(filePath: string): Promise<Bytes> {\n // ssh2-sftp-client\uC758 get()\uC740 dst \uBBF8\uC804\uB2EC \uC2DC Buffer\uB97C \uBC18\uD658\uD55C\uB2E4.\n // \uD0C0\uC785 \uC815\uC758(string | WritableStream | Buffer)\uC640 \uB2EC\uB9AC \uC2E4\uC81C\uB85C\uB294 Buffer\uB9CC \uBC18\uD658\uB41C\uB2E4.\n const result = (await this._requireClient().get(filePath)) as SftpGetResult;\n if (result instanceof Uint8Array) {\n return result;\n }\n // \uD0C0\uC785 \uC815\uC758\uC0C1 string\uB3C4 \uAC00\uB2A5\uD558\uBBC0\uB85C \uBC29\uC5B4 \uCF54\uB4DC\n if (typeof result === \"string\") {\n return new TextEncoder().encode(result);\n }\n throw new SdError(\"\uC608\uC0C1\uCE58 \uBABB\uD55C \uC751\uB2F5 \uD0C0\uC785\uC785\uB2C8\uB2E4.\");\n }\n\n async remove(filePath: string): Promise<void> {\n await this._requireClient().delete(filePath);\n }\n\n /** \uB85C\uCEEC \uD30C\uC77C \uACBD\uB85C \uB610\uB294 \uBC14\uC774\uD2B8 \uB370\uC774\uD130\uB97C \uC6D0\uACA9 \uACBD\uB85C\uC5D0 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4. */\n async put(localPathOrBuffer: string | Bytes, storageFilePath: string): Promise<void> {\n if (typeof localPathOrBuffer === \"string\") {\n await this._requireClient().fastPut(localPathOrBuffer, storageFilePath);\n } else {\n // eslint-disable-next-line no-restricted-globals -- ssh2-sftp-client \uB77C\uC774\uBE0C\uB7EC\uB9AC \uC694\uAD6C\uC0AC\uD56D\n await this._requireClient().put(Buffer.from(localPathOrBuffer), storageFilePath);\n }\n }\n\n async uploadDir(fromPath: string, toPath: string): Promise<void> {\n await this._requireClient().uploadDir(fromPath, toPath);\n }\n\n /**\n * \uC5F0\uACB0\uC744 \uC885\uB8CC\uD569\uB2C8\uB2E4.\n *\n * @remarks\n * \uC774\uBBF8 \uC885\uB8CC\uB41C \uC0C1\uD0DC\uC5D0\uC11C \uD638\uCD9C\uD574\uB3C4 \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.\n * \uC885\uB8CC \uD6C4\uC5D0\uB294 \uB3D9\uC77C \uC778\uC2A4\uD134\uC2A4\uC5D0\uC11C {@link connect}\uB97C \uB2E4\uC2DC \uD638\uCD9C\uD558\uC5EC \uC7AC\uC5F0\uACB0\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n */\n async close(): Promise<void> {\n if (this._client === undefined) {\n return;\n }\n await this._client.end();\n this._client = undefined;\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,eAAe;AACxB,OAAO,gBAAgB;AAahB,MAAM,kBAAqC;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUR,MAAM,QAAQ,QAA0C;AACtD,QAAI,KAAK,YAAY,QAAW;AAC9B,YAAM,IAAI,QAAQ,oJAA2C;AAAA,IAC/D;AAEA,UAAM,SAAS,IAAI,WAAW;AAC9B,QAAI;AACF,YAAM,OAAO,QAAQ;AAAA,QACnB,MAAM,OAAO;AAAA,QACb,MAAM,OAAO;AAAA,QACb,UAAU,OAAO;AAAA,QACjB,UAAU,OAAO;AAAA,MACnB,CAAC;AACD,WAAK,UAAU;AAAA,IACjB,SAAS,KAAK;AACZ,YAAM,OAAO,IAAI;AACjB,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,iBAA6B;AACnC,QAAI,KAAK,YAAY,QAAW;AAC9B,YAAM,IAAI,QAAQ,wFAAuB;AAAA,IAC3C;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAM,MAAM,SAAgC;AAC1C,UAAM,KAAK,eAAe,EAAE,MAAM,SAAS,IAAI;AAAA,EACjD;AAAA,EAEA,MAAM,OAAO,UAAkB,QAA+B;AAC5D,UAAM,KAAK,eAAe,EAAE,OAAO,UAAU,MAAM;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,OAAO,UAAoC;AAC/C,QAAI;AAGF,YAAM,SAAS,MAAM,KAAK,eAAe,EAAE,OAAO,QAAQ;AAC1D,aAAO,OAAO,WAAW;AAAA,IAC3B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,SAAsC;AAClD,UAAM,OAAO,MAAM,KAAK,eAAe,EAAE,KAAK,OAAO;AACrD,WAAO,KAAK,IAAI,CAAC,UAAU;AAAA,MACzB,MAAM,KAAK;AAAA,MACX,QAAQ,KAAK,SAAS;AAAA,IACxB,EAAE;AAAA,EACJ;AAAA,EAEA,MAAM,SAAS,UAAkC;AAG/C,UAAM,SAAU,MAAM,KAAK,eAAe,EAAE,IAAI,QAAQ;AACxD,QAAI,kBAAkB,YAAY;AAChC,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,WAAW,UAAU;AAC9B,aAAO,IAAI,YAAY,EAAE,OAAO,MAAM;AAAA,IACxC;AACA,UAAM,IAAI,QAAQ,8EAAkB;AAAA,EACtC;AAAA,EAEA,MAAM,OAAO,UAAiC;AAC5C,UAAM,KAAK,eAAe,EAAE,OAAO,QAAQ;AAAA,EAC7C;AAAA;AAAA,EAGA,MAAM,IAAI,mBAAmC,iBAAwC;AACnF,QAAI,OAAO,sBAAsB,UAAU;AACzC,YAAM,KAAK,eAAe,EAAE,QAAQ,mBAAmB,eAAe;AAAA,IACxE,OAAO;AAEL,YAAM,KAAK,eAAe,EAAE,IAAI,OAAO,KAAK,iBAAiB,GAAG,eAAe;AAAA,IACjF;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,UAAkB,QAA+B;AAC/D,UAAM,KAAK,eAAe,EAAE,UAAU,UAAU,MAAM;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,QAAuB;AAC3B,QAAI,KAAK,YAAY,QAAW;AAC9B;AAAA,IACF;AACA,UAAM,KAAK,QAAQ,IAAI;AACvB,SAAK,UAAU;AAAA,EACjB;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,74 @@
1
+ import { DateTime } from "./types/date-time";
2
+ import { DateOnly } from "./types/date-only";
3
+ import { Time } from "./types/time";
4
+ import { Uuid } from "./types/uuid";
5
+ /**
6
+ * Buffer 대신 사용하는 바이너리 타입
7
+ */
8
+ export type Bytes = Uint8Array;
9
+ /**
10
+ * Primitive 타입 매핑
11
+ * orm-common과 공유
12
+ */
13
+ export type PrimitiveTypeMap = {
14
+ string: string;
15
+ number: number;
16
+ boolean: boolean;
17
+ DateTime: DateTime;
18
+ DateOnly: DateOnly;
19
+ Time: Time;
20
+ Uuid: Uuid;
21
+ Bytes: Bytes;
22
+ };
23
+ /**
24
+ * Primitive 타입 문자열 키
25
+ */
26
+ export type PrimitiveTypeStr = keyof PrimitiveTypeMap;
27
+ /**
28
+ * Primitive 타입 유니온
29
+ */
30
+ export type PrimitiveType = PrimitiveTypeMap[PrimitiveTypeStr] | undefined;
31
+ /**
32
+ * 깊은 Partial 타입
33
+ *
34
+ * 객체의 모든 속성을 재귀적으로 선택적(optional)으로 만듭니다.
35
+ * Primitive 타입(string, number, boolean 등)은 그대로 유지하고,
36
+ * 객체/배열 타입만 재귀적으로 Partial을 적용합니다.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * interface User {
41
+ * name: string;
42
+ * profile: {
43
+ * age: number;
44
+ * address: { city: string };
45
+ * };
46
+ * }
47
+ *
48
+ * // 모든 깊이의 속성이 선택적이 됨
49
+ * const partial: DeepPartial<User> = {
50
+ * profile: { address: {} }
51
+ * };
52
+ * ```
53
+ */
54
+ export type DeepPartial<T> = Partial<{
55
+ [K in keyof T]: T[K] extends PrimitiveType ? T[K] : DeepPartial<T[K]>;
56
+ }>;
57
+ /**
58
+ * 생성자 타입
59
+ *
60
+ * 클래스 생성자를 타입으로 표현할 때 사용합니다.
61
+ * 주로 의존성 주입, 팩토리 패턴, instanceof 체크 등에서 활용됩니다.
62
+ *
63
+ * @example
64
+ * function create<T>(ctor: Type<T>): T {
65
+ * return new ctor();
66
+ * }
67
+ *
68
+ * class MyClass { name = "test"; }
69
+ * const instance = create(MyClass); // MyClass 인스턴스
70
+ */
71
+ export interface Type<T> extends Function {
72
+ new (...args: unknown[]): T;
73
+ }
74
+ //# sourceMappingURL=common.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.types.d.ts","sourceRoot":"","sources":["../../../../core-common/src/common.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAIpC;;GAEG;AACH,MAAM,MAAM,KAAK,GAAG,UAAU,CAAC;AAM/B;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,KAAK,CAAC;CACd,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,gBAAgB,CAAC;AAEtD;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,gBAAgB,CAAC,gBAAgB,CAAC,GAAG,SAAS,CAAC;AAM3E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,OAAO,CAAC;KAClC,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACtE,CAAC,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,IAAI,CAAC,CAAC,CAAE,SAAQ,QAAQ;IACvC,KAAK,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;CAC7B"}
@@ -0,0 +1,6 @@
1
+ export declare const env: {
2
+ DEV: boolean;
3
+ VER?: string;
4
+ [key: string]: unknown;
5
+ };
6
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../../../core-common/src/env.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,GAAG,EAAE;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAKxB,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { SdError } from "./sd-error";
2
+ /**
3
+ * 인수 오류
4
+ *
5
+ * 잘못된 인수를 받았을 때 발생시키는 에러이다.
6
+ * 인수 객체를 YAML 형식으로 메시지에 포함하여 디버깅을 용이하게 한다.
7
+ *
8
+ * @example
9
+ * // 인수 객체만 전달
10
+ * throw new ArgumentError({ userId: 123, name: null });
11
+ * // 결과 메시지: "인수가 잘못되었습니다.\n\nuserId: 123\nname: null"
12
+ *
13
+ * @example
14
+ * // 커스텀 메시지와 인수 객체 전달
15
+ * throw new ArgumentError("유효하지 않은 사용자", { userId: 123 });
16
+ * // 결과 메시지: "유효하지 않은 사용자\n\nuserId: 123"
17
+ */
18
+ export declare class ArgumentError extends SdError {
19
+ /** 기본 메시지("인수가 잘못되었습니다.")와 함께 인수 객체를 YAML 형식으로 출력 */
20
+ constructor(argObj: Record<string, unknown>);
21
+ /** 커스텀 메시지와 함께 인수 객체를 YAML 형식으로 출력 */
22
+ constructor(message: string, argObj: Record<string, unknown>);
23
+ constructor(arg1: Record<string, unknown> | string, arg2?: Record<string, unknown>);
24
+ }
25
+ //# sourceMappingURL=argument-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"argument-error.d.ts","sourceRoot":"","sources":["../../../../../core-common/src/errors/argument-error.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,aAAc,SAAQ,OAAO;IACxC,qDAAqD;gBACzC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC3C,sCAAsC;gBAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;gBAChD,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAYnF"}