@things-factory/shell 10.1.157 → 10.1.178

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.
@@ -4,7 +4,7 @@
4
4
 
5
5
  ### 1. **클라우드 및 인프라 지원**
6
6
 
7
- Things-Factory 프레임워크는 현대적인 클라우드 인프라와의 완벽한 호환성을 제공하여, 애플리케이션 배포와 관리를 더욱 효율적으로 만듭니다. Edge Computing 기능을 통해 데이터 처리의 효율성을 극대화하며, 다양한 파일 스토리지 옵션을 통해 유연한 데이터 관리를 지원합니다. 클러스터링 환경에서도 높은 성능을 유지할 수 있도록 설계되었습니다.
7
+ Things-Factory 프레임워크는 클라우드 인프라에서 애플리케이션을 배포하고 관리할 수 있도록 지원합니다. Edge Computing 기능으로 현장 데이터를 처리하고, 다양한 파일 스토리지 옵션을 통해 데이터를 관리할 수 있습니다. 실제 처리 성능과 배포 방식은 구성한 인프라에서 확인해야 합니다.
8
8
 
9
9
  - **클라우드 서비스 구조 준비**: Things-Factory는 AWS, GCP, Azure 등 주요 클라우드 플랫폼과 Kubernetes(K8s)와의 원활한 통합을 제공합니다. 이를 통해 사용자는 클라우드 환경에서 애플리케이션을 손쉽게 배포하고 관리할 수 있습니다. 예를 들어, 자동 확장 및 로드 밸런싱 기능을 통해 트래픽 급증 시에도 안정적인 서비스를 유지할 수 있으며, IaC(Infrastructure as Code) 지원으로 클라우드 리소스를 코드로 관리할 수 있습니다.
10
10
  - **Edge Computing**: 클라우드에서 모델링한 데이터를 Edge에서 실행하여, 실시간 데이터 처리와 빠른 응답성을 제공합니다. 예를 들어, 제조 현장에서 발생하는 대량의 센서 데이터를 실시간으로 처리하여, 클라우드에 전송하기 전에 필요한 데이터를 선별하고 필터링할 수 있습니다. 이 기능은 분산된 환경에서도 높은 성능을 유지하며, 네트워크 지연을 최소화할 수 있습니다.
@@ -18,7 +18,7 @@ Things-Factory 프레임워크는 현대적인 클라우드 인프라와의 완
18
18
  - **생체인증 로그인**: 지문이나 얼굴 인식 등 생체인증 기술을 활용하여, 사용자의 보안을 강화합니다. 예를 들어, 지문 인식을 통해 로그인할 때, 사용자는 복잡한 비밀번호를 기억할 필요 없이 간편하게 접근할 수 있습니다. 이를 통해 비밀번호 도난의 위험을 줄이고, 로그인 과정을 간편하게 만듭니다. 생체인증은 은행, 헬스케어 등 보안이 중요한 분야에서 특히 유용합니다.
19
19
  - **SSO 준비**: Google, Azure와 같은 SSO 솔루션을 통합하여, 사용자 인증을 중앙에서 관리할 수 있습니다. 예를 들어, 사용자는 회사 내 여러 애플리케이션에 단일 로그인만으로 접근할 수 있으며, IT 관리자는 사용자의 접근 권한을 일원화하여 관리할 수 있습니다. SSO는 기업 내 보안 수준을 높이고, 사용자 경험을 향상시킵니다.
20
20
  - **OAuth2 지원**: OAuth2 프로토콜을 통해 안전한 인증 및 인가 과정을 제공합니다. 예를 들어, 제3자 애플리케이션이 사용자의 데이터에 접근하려면, 사용자는 OAuth2를 통해 해당 애플리케이션에 접근 권한을 부여할 수 있습니다. 이를 통해 제3자 애플리케이션과의 안전한 데이터 공유가 가능하며, 사용자 권한을 효과적으로 관리할 수 있습니다. OAuth2는 소셜 로그인, API 연동 등 다양한 시나리오에서 활용될 수 있습니다.
21
- - **역할 기반 접근 제어 (RBAC)**: 이 프레임워크는 사전에 정의된 역할과 권한을 기반으로 사용자 접근 관리를 할 수 있는 RBAC 모델 설정을 허용합니다. 예를 들어, 관리자는 '관리자', '편집자', '뷰어'와 같은 역할을 정의할 수 있으며, 각 역할에는 특정 접근 권한과 특권이 부여됩니다. 이를 통해 사용자는 자신의 역할에 필요한 리소스와 기능에만 접근할 수 있어 보안이 강화되고 관리가 간소화됩니다. RBAC 모델은 복잡한 조직 구조를 가진 환경에서 특히 유용하며, 세밀한 접근 제어와 보안 정책 준수를 보장합니다.
21
+ - **역할 기반 접근 제어 (RBAC)**: 이 프레임워크는 사전에 정의된 역할과 권한을 기반으로 사용자 접근 관리를 할 수 있는 RBAC 모델 설정을 허용합니다. 예를 들어, 관리자는 '관리자', '편집자', '뷰어'와 같은 역할을 정의할 수 있으며, 각 역할에는 특정 접근 권한과 특권이 부여됩니다. 이를 통해 사용자는 자신의 역할에 필요한 리소스와 기능에만 접근할 수 있어 보안이 강화되고 관리가 간소화됩니다. RBAC 모델은 복잡한 조직 구조를 가진 환경에서 특히 유용하며, 세밀한 접근 제어와 보안 정책에 따른 접근 제어를 지원합니다.
22
22
 
23
23
  ### 3. **API 및 통신**
24
24
 
@@ -30,22 +30,22 @@ Things-Factory 프레임워크는 현대적인 클라우드 인프라와의 완
30
30
 
31
31
  ### 4. **사용자 인터페이스 및 사용자 경험**
32
32
 
33
- 사용자 인터페이스와 경험은 애플리케이션의 성공에 중요한 요소입니다. Things-Factory 프레임워크는 완벽한 PWA 기능, UI 테마 적용, 반응형 UX, WebComponents 기반 구조, 내장 UI 컴포넌트 등을 통해 최적의 사용자 경험을 제공합니다. 이를 통해 사용자는 직관적이고 효율적인 인터페이스를 사용할 수 있습니다.
33
+ 사용자 인터페이스와 경험은 애플리케이션의 성공에 중요한 요소입니다. Things-Factory 프레임워크는 PWA 기능, UI 테마 적용, 반응형 UX, WebComponents 기반 구조, 내장 UI 컴포넌트 등을 통해 최적의 사용자 경험을 제공합니다. 이를 통해 사용자는 직관적이고 효율적인 인터페이스를 사용할 수 있습니다.
34
34
 
35
35
  - **PWA 기능**: Things-Factory는 완전한 PWA(Progressive Web App) 기능을 지원하여, 웹 애플리케이션이 네이티브 앱과 같은 사용자 경험을 제공합니다. 예를 들어, 사용자는 애플리케이션을 홈 화면에 추가하여 네이티브 앱처럼 사용할 수 있으며, 오프라인 상태에서도 기본적인 기능을 사용할 수 있습니다. 이를 통해 애플리케이션은 오프라인에서도 작동하며, 빠른 로딩 속도와 높은 성능을 유지할 수 있습니다. PWA 기능은 사용자 접근성을 크게 향상시킵니다.
36
36
  - **UI 테마 적용**: Material Theme 컨셉을 적용하여 다양한 애플리케이션에 일관된 테마를 제공합니다. 예를 들어, 다크 모드와 라이트 모드를 지원하여 사용자가 원하는 테마를 선택할 수 있으며, 기업의 브랜드 아이덴티티에 맞춘 커스터마이징이 가능합니다. 이를 통해 사용자 경험을 향상시키고, 브랜드 일관성을 유지할 수 있습니다.
37
37
  - **반응형 UX**: 모바일, 태블릿, 데스크탑 등 다양한 환경에서 최적화된 반응형 UX를 제공합니다. 예를 들어, 사용자는 스마트폰, 태블릿, 데스크탑 등 어떤 기기에서든 동일한 애플리케이션을 일관된 경험으로 사용할 수 있습니다. 이를 통해 사용자는 어떤 기기에서도 일관된 경험을 할 수 있으며, 개발자는 하나의 코드베이스로 여러 플랫폼을 지원할 수 있습니다. 반응형 UX는 개발 효율성과 사용자 만족도를 높입니다.
38
38
  - **WebComponents 기반 구조**: WebComponents 기반의 애플리케이션 구조를 통해, 표준화된 컴포넌트 사용과 확장이 용이합니다. 예를 들어, 개발자는 공통 UI 요소를 WebComponent로 정의하여 재사용하고, 프로젝트 간 일관성을 유지할 수 있습니다. 이를 통해 개발자는 재사용 가능한 컴포넌트를 쉽게 만들고 유지할 수 있습니다. WebComponents는 개발 효율성과 코드 품질을 높입니다.
39
- - **내장 UI 컴포넌트**: 데이터 그리드 등 다양한 UI 컴포넌트를 자체 제공하여, 프레임워크와 완벽히 결합되어 생산성을 향상시킵니다. 예를 들어, 복잡한 데이터 테이블을 쉽게 구현하고, 다양한 필터링 및 정렬 기능을 제공할 수 있습니다. 이를 통해 개발자는 필요한 UI 요소를 빠르게 구현할 수 있으며, 사용자에게 직관적인 인터페이스를 제공합니다. 내장 컴포넌트는 개발 시간과 비용을 절감합니다.
39
+ - **내장 UI 컴포넌트**: 데이터 그리드 등 다양한 UI 컴포넌트를 자체 제공하여, 프레임워크와 연동되어 생산성을 향상시킵니다. 예를 들어, 복잡한 데이터 테이블을 쉽게 구현하고, 다양한 필터링 및 정렬 기능을 제공할 수 있습니다. 이를 통해 개발자는 필요한 UI 요소를 빠르게 구현할 수 있으며, 사용자에게 직관적인 인터페이스를 제공합니다. 내장 컴포넌트는 개발 시간과 비용을 절감합니다.
40
40
  - **그래픽 및 HTML UI 모델러**: 대시보드, Machine Control UI, Floor Control UI 등을 위한 WYSIWYG 기반의 UI 모델러를 제공하여, 생산성을 높입니다. 예를 들어, 사용자는 드래그 앤 드롭 방식으로 대시보드를 구성하고, 실시간 데이터를 시각화할 수 있습니다. 다양한 차트와 비즈니스 컴포넌트를 활용하여, 사용자는 직관적인 그래픽 인터페이스를 만들 수 있습니다. 이 기능은 복잡한 UI를 쉽게 모델링할 수 있게 합니다.
41
41
  - **라벨 모델링 및 데이터바인딩된 라벨 생성**: Things-Factory는 100종류 이상의 다양한 바코드 형식을 지원하며, 사용자가 WYSIWYG 방식으로 라벨을 모델링할 수 있습니다. 예를 들어, 사용자는 드래그 앤 드롭 방식으로 라벨에 텍스트, 이미지, 바코드를 추가하고, 데이터와 바인딩하여 실시간으로 라벨을 생성할 수 있습니다. 이를 통해 바코드 라벨 출력 언어를 몰라도 정확하게 모델링한 모양으로 프린트할 수 있으며, 다양한 폰트와 바코드 형식을 적용한 라벨을 손쉽게 만들 수 있습니다. 이 기능은 물류, 제조, 유통 등 다양한 분야에서 효율적인 라벨링을 가능하게 합니다.
42
42
  - **데이터 분석 및 시각화 도구**: 이 프레임워크는 WYSIWYG 기반 모델링 및 간편한 데이터 바인딩 기능을 포함한 강력한 데이터 분석 및 시각화 도구를 제공합니다. 예를 들어, 사용자는 드래그 앤 드롭 인터페이스를 통해 차트, 그래프, 대시보드와 같은 데이터 시각화를 생성하고 구성할 수 있으며, 이러한 시각적 구성 요소에 데이터를 쉽게 바인딩할 수 있습니다. 이를 통해 사용자는 인터랙티브하고 시각적으로 매력적인 표현을 통해 데이터에서 인사이트를 얻을 수 있으며, 의사 결정 및 데이터 기반 전략을 개선할 수 있습니다.
43
43
 
44
44
  ### 5. **개발 생산성 및 관리**
45
45
 
46
- 효율적인 개발과 관리 도구는 Things-Factory 프레임워크의 핵심입니다. 모듈 구조의 재활용성, 계층적 도메인 구조, 사용자 정의 메뉴 시스템, 다국어 기능 등을 통해 개발 생산성과 관리 효율성을 극대화합니다. 이를 통해 개발자는 더 빠르고 효율적으로 애플리케이션을 구축할 수 있습니다.
46
+ 효율적인 개발과 관리 도구는 Things-Factory 프레임워크의 핵심입니다. 모듈 구조의 재활용성, 계층적 도메인 구조, 사용자 정의 메뉴 시스템, 다국어 기능 등을 통해 개발 생산성과 관리 효율성을 높입니다. 이를 통해 개발자는 더 빠르고 효율적으로 애플리케이션을 구축할 수 있습니다.
47
47
 
48
- - **모듈 구조의 재활용성**: 완벽한 모듈 구조를 통해, 코드를 재활용하고 유지보수하기 쉽게 만듭니다. 예를 들어, 공통 기능을 모듈로 분리하여 여러 프로젝트에서 재사용할 수 있으며, 새로운 기능 추가 시 기존 코드를 수정하지 않고도 쉽게 확장할 수 있습니다. 이를 통해 개발자는 반복적인 작업을 줄이고, 생산성을 극대화할 수 있습니다. 모듈 구조는 애플리케이션의 확장성을 높이고, 품질을 유지합니다.
48
+ - **모듈 구조의 재활용성**: 모듈 구조를 통해, 코드를 재활용하고 유지보수하기 쉽게 만듭니다. 예를 들어, 공통 기능을 모듈로 분리하여 여러 프로젝트에서 재사용할 수 있으며, 새로운 기능 추가 시 기존 코드를 수정하지 않고도 쉽게 확장할 수 있습니다. 이를 통해 개발자는 반복적인 작업을 줄이고, 생산성을 높일 수 있습니다. 모듈 구조는 애플리케이션의 확장성을 높이고, 품질을 유지합니다.
49
49
  - **계층적 도메인 구조**: 상속 가능한 테넌시 구조를 지원하여, 복잡한 다중 도메인 환경에서도 효율적인 관리가 가능합니다. 예를 들어, 여러 하위 도메인이 상위 도메인의 설정과 권한을 상속받아, 일관된 정책을 유지할 수 있습니다. 이를 통해 다양한 비즈니스 요구에 유연하게 대응할 수 있으며, 테넌트 간의 데이터와 설정을 효율적으로 관리할 수 있습니다. 계층적 구조는 관리 효율성을 높입니다.
50
50
  - **사용자 정의 메뉴 시스템 제공**: 각기 다른 사용자 요구에 맞춰 메뉴 시스템을 사용자 정의할 수 있습니다. 예를 들어, 관리자와 일반 사용자에게 각각 다른 메뉴를 제공하여, 필요 없는 기능을 숨기고 중요한 기능을 강조할 수 있습니다. 이를 통해 사용자는 자신에게 최적화된 인터페이스를 만들 수 있으며, 관리자는 다양한 설정을 유연하게 조정할 수 있습니다. 사용자 정의 메뉴 시스템은 사용자 경험을 향상시킵니다.
51
51
  - **다국어 기능**: 테넌시별 사용자 정의 다국어 기능을 통해, 글로벌 비즈니스 환경에서도 원활한 애플리케이션 사용을 보장합니다. 예를 들어, 각 국가별로 언어와 지역 설정을 적용하여, 사용자에게 최적화된 콘텐츠를 제공할 수 있습니다. 이를 통해 각기 다른 언어를 사용하는 사용자들에게 일관된 경험을 제공합니다. 다국어 기능은 글로벌 시장 진출을 용이하게 합니다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/shell",
3
- "version": "10.1.157",
3
+ "version": "10.1.178",
4
4
  "description": "Core module for framework",
5
5
  "bin": {
6
6
  "things-factory": "bin/things-factory",
@@ -67,13 +67,13 @@
67
67
  "@operato/layout": "^10.0.0",
68
68
  "@operato/shell": "^10.0.0",
69
69
  "@operato/typeorm-history": "^10.0.0",
70
- "@operato/utils": "^10.0.0",
70
+ "@operato/utils": "^10.29.36",
71
71
  "@reduxjs/toolkit": "^2.2.5",
72
72
  "@things-factory/ejs-remote": "^10.1.20",
73
73
  "@things-factory/env": "^10.1.149",
74
74
  "@things-factory/operato-license-checker": "^4.0.4",
75
75
  "@things-factory/styles": "^10.1.123",
76
- "@things-factory/utils": "^10.1.20",
76
+ "@things-factory/utils": "^10.1.178",
77
77
  "@types/koa": "^3.0.1",
78
78
  "@types/koa-bodyparser": "^4.3.13",
79
79
  "@webcomponents/scoped-custom-element-registry": "^0.0.9",
@@ -142,5 +142,5 @@
142
142
  "pg": "^8.7.3",
143
143
  "sqlite3": "^5.0.8"
144
144
  },
145
- "gitHead": "bde810f804966549a7041457ad7ede40fcb78488"
145
+ "gitHead": "d57448d3f3045f72a9f24b877a3e306eb34cbaf1"
146
146
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The shell tells every file picker the server's upload limits.
3
+ *
4
+ * ## Why this is tested
5
+ *
6
+ * `ox-input-file` refuses before the upload only when someone registered where the limits come from
7
+ * (@operato/utils `registerFileUploadLimits`). The shell is that someone: on bootstrap it registers a
8
+ * reader of the `fileUploadLimits` query. If the registration is dropped, every picker quietly takes a
9
+ * file the server then refuses after the whole upload.
10
+ */
11
+
12
+ const mockRegistered: Array<() => Promise<any>> = []
13
+ const mockQueries: string[] = []
14
+
15
+ jest.mock('@operato/utils', () => ({ registerFileUploadLimits: (reader: any) => mockRegistered.push(reader) }))
16
+ jest.mock('@operato/layout', () => ({ updateLayout: () => {} }))
17
+ jest.mock('@operato/graphql', () => ({
18
+ client: {
19
+ query: async ({ query }: any) => {
20
+ mockQueries.push(query.loc.source.body)
21
+ return { data: { fileUploadLimits: { maxFileSize: 1024, maxFiles: 3 } } }
22
+ }
23
+ }
24
+ }))
25
+
26
+ describe('client bootstrap', () => {
27
+ it('registers the server upload limits for every file picker', async () => {
28
+ /* The module writes its version on window when loaded. */
29
+ ;(global as any).window = { matchMedia: () => ({ matches: true, addEventListener: () => {} }) }
30
+ const bootstrap = require('../client/bootstrap').default
31
+
32
+ bootstrap()
33
+
34
+ expect(mockRegistered.length).toBe(1)
35
+ expect(await mockRegistered[0]()).toEqual({ maxFileSize: 1024, maxFiles: 3 })
36
+ expect(mockQueries[0]).toMatch(/fileUploadLimits\s*\{\s*maxFileSize\s+maxFiles\s*\}/)
37
+ })
38
+ })
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The upload limits, in one place.
3
+ *
4
+ * ## Why this is tested
5
+ *
6
+ * The upload middleware enforces `fileUpload.maxFileSize` / `maxFiles`; the `fileUploadLimits`
7
+ * query hands the same numbers to every file picker so it refuses before uploading. Both servers
8
+ * (server.ts, server-dev.ts) once carried their own copy of the reading. If the query and the
9
+ * middleware drift, a picker takes a file the server then refuses.
10
+ */
11
+ import 'reflect-metadata'
12
+
13
+ const mockConfig: Record<string, any> = {}
14
+
15
+ jest.mock('@things-factory/env', () => ({ config: { get: (key: string) => mockConfig[key] } }))
16
+
17
+ import { fileUploadOption } from '../server/service/file-upload/file-upload-option'
18
+ import { FileUploadQuery } from '../server/service/file-upload/file-upload-query'
19
+
20
+ describe('upload limits', () => {
21
+ afterEach(() => delete mockConfig.fileUpload)
22
+
23
+ it('falls back to 10mb and 10 files', () => {
24
+ expect(fileUploadOption()).toEqual({ maxFileSize: 10 * 1024 * 1024, maxFiles: 10 })
25
+ })
26
+
27
+ it('reads the configured limits', () => {
28
+ mockConfig.fileUpload = { maxFileSize: '25mb', maxFiles: 3 }
29
+ expect(fileUploadOption()).toEqual({ maxFileSize: 25 * 1024 * 1024, maxFiles: 3 })
30
+ })
31
+
32
+ it('answers the picker with the numbers the middleware enforces', () => {
33
+ mockConfig.fileUpload = { maxFileSize: '2mb', maxFiles: 4 }
34
+ expect(new FileUploadQuery().fileUploadLimits()).toEqual(fileUploadOption())
35
+ })
36
+ })
37
+
38
+ describe('both servers', () => {
39
+ const fs = require('fs')
40
+ const path = require('path')
41
+
42
+ it.each(['server.ts', 'server-dev.ts'])('%s reads the limits from the one place', name => {
43
+ const source = fs.readFileSync(path.join(__dirname, '../server', name), 'utf8')
44
+
45
+ expect(source).toContain('graphqlUploadKoa(fileUploadOption())')
46
+ expect(source).not.toContain("config.get('fileUpload')")
47
+ })
48
+ })
@@ -0,0 +1,52 @@
1
+ /**
2
+ * One file picker.
3
+ *
4
+ * Picking a file was written eight times across operato and things-factory. `ox-input-file`
5
+ * (@operato/input) is now the one picker and `FileDropHelper` (@operato/utils) the one drop helper.
6
+ * This counts that the retired copies stay gone from things-factory client code: the form-ui
7
+ * `file-uploader`, the image-uploader-ui `image-uploader` and the second `FileDropHelper`.
8
+ *
9
+ * A new need goes into ox-input-file, not into a new element here.
10
+ *
11
+ * It also asserts how many files it read, so a wrong path cannot pass by reading nothing.
12
+ */
13
+
14
+ import { readFileSync, readdirSync, statSync } from 'fs'
15
+ import path from 'path'
16
+
17
+ const PACKAGES = path.resolve(__dirname, '../..')
18
+ const SKIP = new Set(['node_modules', 'dist', 'dist-client', 'dist-server', 'coverage'])
19
+
20
+ function sources(dir: string, found: string[] = []): string[] {
21
+ for (const name of readdirSync(dir)) {
22
+ if (SKIP.has(name) || name.startsWith('.')) continue
23
+ const full = path.join(dir, name)
24
+ const stat = statSync(full)
25
+ if (stat.isDirectory()) sources(full, found)
26
+ else if (/\.(ts|js)$/.test(name)) found.push(full)
27
+ }
28
+ return found
29
+ }
30
+
31
+ const RETIRED: [string, RegExp][] = [
32
+ ['form-ui file-uploader', /customElements\.define\(\s*['"]file-uploader['"]|<file-uploader[\s>]/],
33
+ ['image-uploader-ui image-uploader', /customElements\.define\(\s*['"]image-uploader['"]|<image-uploader[\s>]/],
34
+ ['a second FileDropHelper', /class\s+FileDropHelper\b/]
35
+ ]
36
+
37
+ describe('one file picker', () => {
38
+ const files = readdirSync(PACKAGES)
39
+ .map(one => path.join(PACKAGES, one))
40
+ .filter(dir => statSync(dir).isDirectory())
41
+ .flatMap(dir => ['client', 'src'].map(sub => path.join(dir, sub)).filter(sub => statSync(sub, { throwIfNoEntry: false })?.isDirectory()))
42
+ .flatMap(dir => sources(dir))
43
+
44
+ it('reads the client code of every package', () => {
45
+ expect(files.length).toBeGreaterThan(1000)
46
+ })
47
+
48
+ it.each(RETIRED)('%s is gone', (_, pattern) => {
49
+ const found = files.filter(file => pattern.test(readFileSync(file, 'utf8'))).map(file => path.relative(PACKAGES, file))
50
+ expect(found).toEqual([])
51
+ })
52
+ })
@@ -180,7 +180,7 @@
180
180
  "button.hide-end-pane": "오른쪽 판 접기",
181
181
  "button.show-end-pane": "오른쪽 판 펼치기",
182
182
  "title.unsaved-draft": "저장하지 않은 편집이 있습니다",
183
- "text.unsaved-draft-kept": "{minutes}분 전에 고친 내용이 이 브라우저에 남아 있습니다.",
183
+ "text.unsaved-draft-kept": "{minutes}분 전에 수정한 내용이 이 브라우저에 남아 있습니다.",
184
184
  "text.unsaved-draft-behind": "그 뒤에 다시 저장되었습니다. 저장할 때 충돌을 알려 드립니다.",
185
185
  "button.resume-editing": "이어서 편집",
186
186
  "button.discard": "버리기",