Opkle(옵클) - 창작자를 위한 앱과 시스템
옵클(Opkle)은 창작자를 위한 다양한 앱과 시스템을 제공하는 개발사입니다. 전자책 에디터 앱 'Opkle editor'를 출시했고, 관련 전자책 클래스를 제공하고 있습니다.
EDITOR
CLASS
BLOG
LOGIN
표현한다는 것의
무한한 가능성,
새로운 형태로
담아내다.
새로운 형태의 콘텐츠
Opkle은 코드 없이 웹을 마음껏 만들고, 누구나 자기 화면을 그릴 수 있도록 에디터를 만들고, 그 결과물을 어디서나 즐길 수 있게 돕는 팀입니다.
텍스트와 화면과의 조화를 통해, 웹을 짓는다는 것이 그저 단순한 코딩이 아닌, 상상을 펼치고 감각을 깨우는 과정이 될 수 있도록 좋은 도구를 만들어 냅니다.
옵클 에디터 개발기: 그래픽 엔진 안의 선형대수
dev
12
옵클 에디터 개발기: 인디자인처럼 편집하는 레이아웃 편집기
dev
13
옵클 에디터 개발기: 원고가 다음 페이지로 흐르는 편집기
dev
14
옵클 에디터 개발기: EPUB과 IDML을 편집 가능한 페이지로
dev
15
옵클 에디터 개발기: 한 번 만든 레이아웃을 EPUB과 인쇄물로
dev
16
8
9
10
11
12
...
15
옵클 에디터 개발기: EPUB과 IDML을 편집 가능한 페이지로
레이아웃과 조판 엔진이 완성된 다음에는 이 편집기를 빈 문서에서만 시작하는 도구로 남겨둘 이유가 없었습니다. 실제 출판 작업에는 이미 작성된 원고와 기존 EPUB, 다른 조판 프로그램에서 만든 페이지가 계속 들어옵니다. 파일을 화면에 보여주는 것만으로는 부족했고, 그 안의 글과 이미지, 스타일, 읽기 순서, 페이지 관계를 다시 편집 가능한 상태로 복원해야 했습니다.
포맷마다 문서를 설명하는 방식은 완전히 다릅니다. EPUB은 HTML과 CSS, manifest와 spine으로 읽기 흐름을 정의합니다. IDML은 여러 XML 사이에 spread, master, story, frame과 transform을 나눠 저장합니다. TXT와 DOCX, HWP 같은 일반 원고는 페이지보다 텍스트 자체가 중요합니다. 이들을 하나의 평평한 HTML로 바꾸면 가져오기는 간단해지지만 원래 문서가 가지고 있던 의미가 사라집니다.
그래서 입력 포맷별 parser는 분리하고, 최종적으로 레이아웃 에디터가 이해하는 페이지·텍스트 프레임·이미지·스타일·자산으로 단계적으로 내렸습니다. EPUB의 cascade는 cascade대로 계산하고, IDML의 좌표는 행렬로 해석한 뒤에야 native object로 바꿨습니다. 이 편집기의 importer는 파일 확장자를 여는 기능이 아니라 서로 다른 문서 모델을 옵클의 편집 모델로 번역하는 엔진이 되었습니다.
들어오는 문서들
입력의 첫 단계는 파일 종류를 판별하고 적절한 경로로 보내는 것이었습니다. JPEG와 PNG는 사용자가 떨어뜨린 page local 좌표에 이미지 오브젝트로 들어가고, 폰트는 프로젝트 자산과 `FontFace`로 등록됩니다. EPUB과 프로젝트 package는 압축 문서 parser로, IDML은 XML package parser로, TXT와 DOCX, HWP, HWPX는 공통 문서 판독 계층으로 보냈습니다.
같은 drag and drop이라도 먼저 대상 페이지를 확정해야 했습니다. 마우스의 screen point와 각 page box를 비교해 어느 페이지 위에 떨어졌는지 찾고, 그 페이지를 활성화한 다음 local 좌표로 변환합니다. 이전 페이지가 활성화된 상태에서 파일만 삽입하면 DOM은 이전 layer에 들어가고 새 위치는 다른 페이지 기준으로 계산되어 clip 바깥에서 사라질 수 있습니다. 파일 해석보다 문서 안의 소유권을 먼저 결정했습니다.
일반 원고 파일은 복잡한 원본 레이아웃을 억지로 모사하지 않았습니다. 공통 판독기에서 텍스트를 추출하고 줄바꿈과 연속 빈 줄을 정규화한 뒤 단락 배열로 만들었습니다. 현재 판형의 마진 영역에 첫 본문 프레임을 만들고, 원고를 넣은 다음 넘침이 없어질 때까지 페이지와 연결 프레임을 추가했습니다. TXT와 워드프로세서 문서가 같은 텍스트 조판 경로를 공유합니다.
단락에 제목, 장, 소제목, 본문, 인용과 캡션 같은 역할 정보가 있으면 역할별 paragraph style을 한 번 생성해 재사용하도록 했습니다. 분류 결과의 수와 실제 단락 수가 다르거나 중간 해석이 실패하면 균일한 본문 스타일로 돌아갑니다. 가져오기 자동화가 완전하지 않은 문서에서도 원고 자체는 반드시 남도록 fallback을 두었습니다.
EPUB package
EPUB은 zip을 푸는 것부터 시작하지만 압축 안의 파일 순서가 곧 책의 읽기 순서는 아닙니다. 먼저 `mimetype`이 있다면 `application/epub+zip`인지 확인하고, `META-INF/container.xml`의 rootfile에서 OPF 위치를 찾았습니다. OPF가 하위 폴더에 있을 수 있으므로 이후 모든 href는 그 디렉터리를 base로 정규화했습니다.
OPF의 manifest를 읽어 id, href, media type, properties와 zip 내부 절대 경로를 가진 map으로 만들었습니다. `../`와 `./`가 포함된 상대 경로도 같은 자산을 가리키도록 해소했습니다. 그다음 spine의 `itemref` 순서를 manifest와 join해 실제 XHTML 본문 배열을 만들었습니다. 파일 시스템 순서가 아니라 제작자가 지정한 reading order가 importer의 기준이 됩니다.
title, creator, language 같은 metadata는 XML namespace prefix 문자열에 의존하지 않았습니다. `dc:title`처럼 prefix가 달라져도 local name이 같으면 같은 필드로 읽었습니다. cover image와 nav처럼 manifest properties에 들어 있는 역할도 함께 보관했습니다. 본문과 자산을 찾는 모든 경로는 zip entry를 미리 만든 절대 경로 map에서 조회해 반복 탐색을 없앴습니다.
프로젝트 package는 용량을 줄이기 위해 기본 폰트나 기본 표지를 생략할 수 있습니다. OPF에는 참조가 남아 있는데 실제 blob이 없는 경우에만 기본 자산 저장소에서 해당 파일을 복원해 같은 path map에 주입했습니다. 외부 EPUB이나 사용자가 교체한 커스텀 자산은 이미 blob이 있으므로 건드리지 않습니다. manifest가 가리키는 대상과 실제 바이너리가 일치하는 상태를 먼저 만든 뒤 본문 흡수를 시작했습니다.
Cascade 계산
EPUB의 CSS를 단순한 문자열 하나로 합치면 어떤 declaration이 최종적으로 이겼는지 알 수 없습니다. detached document에 style sheet를 만들어 브라우저 CSSOM으로 rule을 읽고, 콤마로 묶인 selector는 개별 rule로 펼쳤습니다. 각 rule에는 declaration map과 specificity, 전체 stylesheet에서의 source order를 함께 기록했습니다.
specificity는 id, class·attribute·pseudo-class, tag·pseudo-element를 서로 다른 자릿수로 계산했습니다. element의 inline style을 가장 높은 우선순위로 두고, selector match가 성공한 rule들을 specificity와 source order 순으로 비교했습니다. specificity가 같으면 나중에 나온 rule이 이기는 원래 cascade를 유지했습니다. 상속 가능한 typography 속성은 부모에서 확정된 값과 element 자신의 rule을 구분해 계산했습니다.
`@font-face`는 일반 selector rule과 분리해 family, weight, style의 합성 key로 모았습니다. CSS의 상대 URL은 stylesheet 위치를 기준으로 zip 내부 경로를 해소하고 실제 font blob과 연결했습니다. 같은 family라도 400, 700과 italic은 서로 다른 face로 등록했습니다. 폰트가 실제로 준비된 뒤에야 텍스트 geometry를 측정하도록 로딩 경계도 함께 만들었습니다.
미디어쿼리는 importer가 사용할 명시적인 렌더 profile로 평가했습니다. 옵클 EPUB의 태블릿·라이트 상태를 기준으로 약 `49.5rem`의 viewport를 두고 `min-width`, `max-width`, orientation과 `prefers-color-scheme` 조건을 계산했습니다. comma branch는 OR, branch 안의 `and`는 AND로 처리하고 dark와 print 분기는 제외했습니다. desktop 브라우저에서 importer를 실행해도 선택된 EPUB profile의 CSS가 일관되게 활성화됩니다.
Shadow pagination
CSS가 확정된 본문은 바로 페이지 오브젝트로 자르지 않고 보이지 않는 측정용 Shadow DOM에 먼저 렌더했습니다. 메인 에디터의 reset이나 toolbar style이 EPUB 본문에 섞이지 않게 격리하고, 해석한 CSS와 font face, image URL을 shadow 안에 넣었습니다. 실제 content width와 height를 사용해 column width는 페이지 본문 폭으로, column gap은 0으로 맞춘 multi-column layout을 만들었습니다.
폰트가 준비되기 전에 geometry를 읽으면 fallback font의 줄바꿈으로 페이지 수가 결정됩니다. `document.fonts.ready`와 필요한 face의 load가 끝난 뒤 두 번의 animation frame을 지나 layout을 확정했습니다. 각 leaf가 어느 column에 놓였는지, 그 column 안의 x와 y, 실제 width와 height가 얼마인지 rect를 읽어 page index와 local bounds로 변환했습니다.
block element의 단일 rect만으로는 여러 column에 걸친 실제 줄 조각이 빠질 수 있습니다. 텍스트 node에는 `Range`를 만들어 `getClientRects()`를 읽고, 줄 단위 fragment가 어느 페이지에 속하는지 계산했습니다. zoom이 적용된 편집 화면의 rect와 측정용 원본 geometry를 섞지 않도록 측정 단계는 항상 배율 1의 독립 공간에서 수행했습니다.
이미지는 일반 SVG image로 바꾸고 원본 바이너리를 영구 blob 자산에 등록했습니다. 텍스트 leaf는 편집 가능한 `foreignObject`로 만들되 안쪽에는 독립된 Shadow DOM을 유지했습니다. 바깥에서는 선택과 이동, resize가 가능한 레이아웃 오브젝트이고, 안쪽에서는 EPUB의 selector와 pseudo style, media rule이 에디터 CSS와 충돌하지 않고 그대로 작동합니다.
Fragment의 보존
원래 HTML에서 하나의 element였던 본문을 leaf 단위로 분리하면 ancestor selector가 끊어집니다. `body .chapter p`로 적용된 스타일은 `p`만 새 Shadow DOM에 넣는 순간 더 이상 match하지 않습니다. 그래서 원본 구조에서 계산된 typography와 box style을 새 element에 재귀적으로 옮기고, 동시에 원본 scoped CSS도 shadow 안에 유지했습니다. 구조에 의한 규칙과 최종 computed 결과를 함께 보존했습니다.
같은 element가 여러 페이지에 걸쳐 잘리면 첫 fragment와 continuation fragment를 구분했습니다. 전체 원문은 첫 조각에 권위적으로 남기고 나머지는 같은 source id와 fragment index를 가집니다. 판형을 다시 계산할 때 화면에 잘린 조각을 이어 붙이는 대신 보존된 전체 본문을 새 content box에서 다시 측정할 수 있습니다. 현재 화면의 결과와 재흐름 source를 분리한 것입니다.
텍스트 박스의 높이는 CSS line box만 믿지 않고 실제 glyph의 ink bounds까지 확인했습니다. italic overhang과 ascender, descender가 `foreignObject` 경계에 걸리면 글자 끝이 잘릴 수 있습니다. text node range가 차지하는 위아래 범위를 읽어 clip 창과 box 위치를 미세하게 보정하고, margin collapse가 들어간 leaf는 첫 line의 실제 시작점을 기준으로 다시 맞췄습니다.
EPUB의 body style을 레이아웃 안에서 바꿀 때도 일반 DOM 순회로는 shadow 내부에 닿지 않습니다. 선택한 박스의 override와 문서 기본 style을 별도로 기록하고, shadow wrapper를 다시 만들거나 프로젝트를 열 때 같은 cascade를 재적용했습니다. 가져온 본문이 독립적으로 보이기만 하는 것이 아니라 레이아웃 에디터의 문단 편집 기능과도 연결되도록 했습니다.
텍스트만 들어 있고 여러 페이지로 분절되지 않은 박스는 흡수 후 native 텍스트 프레임으로 한 번 더 승격했습니다. heading과 paragraph, list를 에디터의 문단 모델로 정규화하고 inline style은 중복을 제거한 문자 style span으로 옮겼습니다. 변환 뒤에는 EPUB 전용 marker를 제거하고 native 렌더 높이가 더 크면 프레임을 늘려 잘림을 막았습니다. 이미지·표·SVG가 섞였거나 여러 fragment로 나뉜 박스만 Shadow DOM을 유지해 복잡한 원본 표현과 직접 편집성을 모두 보존했습니다.
Shadow serialization
Shadow DOM은 host의 `outerHTML`에 포함되지 않습니다. 화면에서 편집한 내용이 shadowRoot 안에만 있으면 페이지 문자열을 저장하는 순간 본문이 사라집니다. 각 EPUB 텍스트 오브젝트에 wrapper HTML과 scoped CSS를 직렬화 가능한 fallback attribute로 함께 남겼습니다. 자동 저장 직전에는 live shadow의 최신 내용을 fallback과 동기화했습니다.
프로젝트를 다시 열 때는 먼저 host와 page DOM을 복원하고, 저장된 wrapper HTML과 CSS로 shadowRoot를 다시 조립합니다. style element와 editable wrapper를 정해진 순서로 넣고, paragraph override와 fragment metadata를 적용합니다. 과거 버전처럼 fallback이 충분하지 않은 데이터에서는 원본 EPUB master body나 leaf 속성에서 복원 가능한 내용을 다시 구성했습니다.
blob URL은 브라우저 세션을 넘을 수 없으므로 영속 데이터로 저장하지 않았습니다. image element에는 프로젝트 blob id를 남기고, CSS의 `url(...)`은 저장 가능한 data URL 또는 다시 해석할 수 있는 자산 참조로 바꿨습니다. load 시 IndexedDB에서 blob을 읽어 새 object URL을 만들고 image href와 shadow CSS를 모두 다시 연결했습니다.
사용이 끝난 object URL은 해제하되 아직 다른 페이지가 참조하는 blob 자체는 지우지 않았습니다. 저장, lazy load, 판형 reflow가 서로 다른 시점에 자산을 다시 요청할 수 있기 때문입니다. live DOM, 직렬화 문자열, 영구 blob의 세 수명을 분리한 뒤 EPUB 본문과 이미지가 앱 재시작 이후에도 같은 모양으로 돌아왔습니다.
IDML document graph
IDML은 EPUB과 반대 방향의 문서였습니다. HTML 흐름 대신 package 안의 여러 XML이 참조 관계로 페이지를 설명합니다. design map에서 spread와 master spread, story와 resource 문서의 위치를 읽고, 색상 swatch와 gradient, paragraph·character style을 먼저 map으로 만들었습니다. 이후 page item의 id가 이 resource들을 참조할 수 있게 했습니다.
Story XML에는 실제 원고와 paragraph style range, character style range, 강제 줄바꿈, 표와 drop cap 정보가 들어 있습니다. Spread XML에는 page와 text frame, rectangle, ellipse, polygon, line, image가 배치되어 있고 frame은 parent story id만 가집니다. 원고와 프레임이 같은 파일에 있지 않으므로 id를 따라 story와 frame을 다시 결합해야 합니다.
Master spread는 일반 spread와 같은 page item parser를 사용하되 결과를 별도의 master map으로 보냈습니다. 일반 페이지는 applied master id를 보관하고, import가 끝난 뒤 좌우 master page를 옵클의 master DOM으로 변환해 연결했습니다. 자동 쪽번호 표식은 현재 page context에서 실제 번호로 평가할 수 있는 변수로 옮겼습니다.
표는 row와 column의 물리 크기, cell span, inset, stroke와 fill을 읽어 편집 가능한 HTML table 구조로 변환했습니다. 세로쓰기 story는 일반 가로 story와 다른 진행 방향을 보존하고, 포함된 이미지와 폰트는 EPUB과 같은 영구 자산 계층에 넣었습니다. IDML을 한 장의 배경 이미지로 가져오는 것이 아니라 페이지를 다시 구성할 수 있는 document graph로 해석했습니다.
Affine coordinate
IDML 좌표의 기본 단위는 point이고 SVG world와 맞출 때는 `96 / 72` 비율을 사용했습니다. 하지만 단위 변환만으로는 오브젝트 위치가 나오지 않습니다. 각 item은 `[a, b, c, d, tx, ty]` 형태의 2차원 affine matrix를 가지고, 점 `(x, y)`는 `(ax + cy + tx, bx + dy + ty)`로 이동합니다.
group 안에 들어 있는 item은 자기 행렬만 적용해서는 spread 위치를 알 수 없습니다. column vector convention을 고정하고 `M_spread M_group M_item` 순서로 부모 변환을 합성했습니다. 페이지가 spread 안에서 이동하거나 회전되어 있다면 page transform의 inverse를 곱해 최종 점을 페이지 왼쪽 위 기준의 local 좌표로 돌렸습니다.
역행렬을 만들기 전에는 determinant `ad - bc`가 0에 가까운지 확인했습니다. 거의 singular한 변환을 그대로 뒤집으면 작은 오차가 매우 큰 좌표로 증폭됩니다. 변환된 네 모서리에서 위쪽 edge와 오른쪽 edge의 dot product를 구하고 두 벡터가 거의 수직이면 width, height와 rotation을 가진 일반 오브젝트로 분해했습니다.
shear가 포함되어 직각성이 깨진 사각형은 억지로 rotation만 가진 rect로 단순화하지 않았습니다. 네 모서리를 실제로 변환한 polygon path로 보존했습니다. 자유 path는 anchor와 left·right direction handle을 모두 page local 좌표로 바꾸고 subpath의 open·closed 상태를 유지했습니다. IDML의 변환 결과를 axis-aligned box로 줄이지 않아 기울어진 도형과 Bezier geometry가 원본과 맞았습니다.
Story와 style
IDML story는 여러 text frame에 같은 원고를 복사해 두지 않습니다. 전체 원고는 story 한 곳에 있고 프레임의 이전·다음 참조가 배치 순서를 결정합니다. importer에서는 프레임의 연결 그래프를 먼저 만든 뒤 head frame에 story 전체를 넣고, 옵클의 text reflow engine이 뒤쪽 프레임으로 내용을 분배하게 했습니다.
paragraph range에서는 정렬과 leading, tracking, 첫 줄 들여쓰기, 좌우 들여쓰기, 문단 앞뒤 간격을 읽었습니다. character range는 font family와 style, point size, baseline shift, underline, fill과 stroke를 span으로 변환했습니다. 수치 단위는 point에서 SVG world로 바꾸고 line-height는 font size에 대한 비율로 정규화했습니다. 원고 문자열과 typographic run을 같은 DOM에 복원했습니다.
CMYK swatch는 편집 화면에서 사용할 RGB 값으로 변환하되 원래 색상 정의와 이름을 style resource에 남겼습니다. gradient는 stop 위치와 색을 SVG definition으로 만들고, image frame의 crop과 transform을 실제 image bounds에 적용했습니다. frame의 geometry와 content geometry를 분리해 이미지가 박스 안에서 원래 잘린 상태를 유지하도록 했습니다.
story thread와 master, page item, table, swatch를 모두 연결한 다음 빈 참조와 cycle을 검사했습니다. importer가 만든 텍스트 프레임도 일반 조판 엔진의 연결 규칙을 통과시키고, style을 적용한 뒤 전체 chain을 한 번만 reflow했습니다. 다른 프로그램의 문서가 옵클 안에서 선택·수정·재조판 가능한 native page로 복원되었습니다.
판형을 다시 계산하는 법
가져온 문서를 편집할 수 있게 된 뒤에는 판형과 마진을 바꾸는 경로도 같은 수준으로 맞췄습니다. width와 height만 줄이면 텍스트와 EPUB fragment가 이전 페이지 경계를 그대로 가진 채 잘립니다. 반대로 모든 오브젝트를 단순 비율로 줄이면 원고의 reading order와 새 마진의 의미가 사라집니다. 내용의 종류에 따라 다른 재배치가 필요했습니다.
재흐름 전에는 lazy 상태의 모든 페이지를 강제로 로드했습니다. 화면에 없는 페이지의 DOM을 읽지 않고 reset하면 그 안의 원고와 수동 오브젝트를 수집할 수 없습니다. 전체 load가 끝난 뒤 문서를 EPUB 흐름, native text 흐름, 수동 배치 문서로 분류하고, 권위적인 원문과 문단 clone, 수동 오브젝트, 페이지 장식과 여백 영역의 style을 각각 수집했습니다.
EPUB은 보존한 전체 body와 scoped CSS를 새 content width와 height에서 다시 측정했습니다. native text는 `textContent`로 평탄화하지 않고 paragraph node 자체를 clone해 링크와 inline style, paragraph style id를 유지한 채 새 마진 프레임에 넣었습니다. 수동 오브젝트는 `sx = newWidth / oldWidth`, `sy = newHeight / oldHeight`로 좌표와 크기를 조정해 원래 page index에 가깝게 돌려보냈습니다.
페이지 배경이나 여백 영역은 수동 오브젝트처럼 scale하지 않았습니다. 새 판형과 새 마진에서 geometry를 다시 만들고 fill과 stroke 같은 의미 있는 style만 승계했습니다. 모든 페이지가 같은 장식을 쓰고 있었다면 새로 늘어난 페이지에도 적용하고, 페이지별로 달랐다면 같은 index 범위에서만 복원했습니다. 재흐름 결과가 원래 페이지 수보다 적어도 수동 오브젝트가 마지막 페이지에 뭉치지 않도록 먼저 빈 페이지를 보충했습니다.
배치 결과가 0이라면 새 상태를 저장하지 않았습니다. 화면이 이미 reset되었더라도 마지막 정상 영속 상태를 남겨 새로고침으로 복구할 수 있게 했습니다. 일반 페이지 history는 format barrier에서 정리하고 이전과 다음 판형은 별도의 규격 history로 관리해 undo와 redo에서도 같은 pipeline을 다시 실행했습니다. 문서 전체를 바꾸는 작업을 부분 snapshot으로 복원하지 않았습니다.
Import engine 완성
이 단계에서 레이아웃 에디터는 서로 다른 문서를 자신의 편집 모델로 흡수할 수 있게 되었습니다. 일반 원고는 현재 판형의 본문 frame과 text thread로 들어오고, EPUB은 manifest와 spine의 reading order, CSS cascade와 media profile을 유지한 채 page fragment로 변환됩니다. IDML은 spread와 master, story, affine transform과 resource graph를 가진 native object로 복원됩니다.
가져온 결과는 일회성 화면이 아니었습니다. Shadow DOM의 본문과 scoped CSS를 직렬화하고, 세션성 blob URL을 영구 자산 id로 바꾸며, story와 frame의 연결을 일반 조판 엔진에 통합했습니다. 판형을 바꿀 때도 원문과 수동 오브젝트, 페이지 장식을 구분해 새 규격으로 다시 계산할 수 있습니다.
외부 EPUB과 프로젝트 package, 여러 CSS와 폰트 조합, 긴 문단과 이미지가 섞인 본문, master와 group transform이 있는 IDML, 일반 원고와 수동 배치가 함께 있는 문서를 반복해 흡수했습니다. 앱 재시작과 lazy reload, 판형 변경 뒤에도 읽기 순서와 이미지, 글꼴, 문단 스타일, page geometry가 유지되는 것을 모두 검수했습니다. 서로 다른 출판 포맷을 다시 편집 가능한 페이지로 바꾸는 import engine은 완전하게 구현되고 안정화되었습니다.
이전글
목록으로
다음글
저작권 고시
Copyright Notice
본 웹사이트의 모든 디자인 결과물 및 영상에 대한 저작권은 Abstract Cloud에 있으며, 저작권법 및 관련 법령에 의해 보호받습니다. 웹, 영상, 본문, 표지, 내지 디자인을 포함한 모든 콘텐츠는 저작권자의 자산으로, 사전 동의 없이 무단 복제, 배포, 2차 저작물 제작, 온라인 공유 등을 금지합니다. 이를 위반할 시, 저작권법에 따라 민형사상 책임을 질 수 있습니다. 정당한 구매와 저작권 보호는 창작자의 권리를 지키며, 더 나은 작품으로 보답할 힘이 됩니다.
저작권자: Abstract Cloud | 대표자: 배창규(uragen)
© Abstract Cloud. All Rights Reserved.
HOME
FAQ
이용 약관
개인정보 이용방침
help@opkle.app
010-2747-3403
상호 :
추상적 형상 디자인(Abstract cloud) |
대표자 :
배창규
사업자등록번호 :
249-74-00533
통신판매업 신고번호 :
2025-의정부송산-0634
주소 :
경기도 의정부시 부용로 49, 108동 402호
웹의 모든 콘텐츠, 디자인, 소스 코드에 대한
저작권은 Opkle에게 있습니다.