EDITORCLASSBLOGLOGINimg default descriptionimg default descriptionimg default description표현한다는 것의무한한 가능성,새로운 형태로
담아내다.
img default descriptionimg default descriptionimg default descriptionimg default descriptionimg default descriptionimg default descriptionimg default description새로운 형태의 콘텐츠Opkle은 코드 없이 웹을 마음껏 만들고, 누구나 자기 화면을 그릴 수 있도록 에디터를 만들고, 그 결과물을 어디서나 즐길 수 있게 돕는 팀입니다.텍스트와 화면과의 조화를 통해, 웹을 짓는다는 것이 그저 단순한 코딩이 아닌, 상상을 펼치고 감각을 깨우는 과정이 될 수 있도록 좋은 도구를 만들어 냅니다.
옵클 에디터 개발기: 그래픽 엔진 안의 선형대수dev12옵클 에디터 개발기: 인디자인처럼 편집하는 레이아웃 편집기dev13옵클 에디터 개발기: 원고가 다음 페이지로 흐르는 편집기dev14옵클 에디터 개발기: EPUB과 IDML을 편집 가능한 페이지로dev15옵클 에디터 개발기: 한 번 만든 레이아웃을 EPUB과 인쇄물로dev1689101112...14 옵클 에디터 개발기: 원고가 다음 페이지로 흐르는 편집기페이지 공간을 완성하고 나니 레이아웃 에디터의 다음 성격이 분명해졌습니다. 도형과 이미지를 정확한 위치에 놓는 것만으로는 책을 편집할 수 없습니다. 원고는 계속 바뀌고, 한 문장이 늘어나면 현재 프레임의 마지막 줄뿐 아니라 그 뒤에 연결된 모든 페이지의 배치가 달라집니다. 판형과 글꼴, 행간, 자간이 바뀌어도 같은 일이 일어납니다.브라우저에는 HTML을 화면 폭에 맞춰 흐르게 하는 layout engine이 이미 있지만, 출판용 페이지의 텍스트 프레임은 브라우저 문서와 같지 않습니다. 서로 떨어진 여러 `foreignObject`가 하나의 story를 공유해야 하고, 문단의 일부만 다음 페이지로 넘어갈 수 있어야 합니다. 사용자가 중간 페이지에서 글을 고치는 동안 caret과 한글 IME를 유지하면서 뒤쪽 프레임을 실시간으로 다시 계산해야 합니다.저는 이 조판을 텍스트만의 기능으로 고립시키지 않았습니다. 페이지 안에는 자유 좌표로 놓인 오브젝트와 Flex로 흐르는 묶음이 함께 있고, 모든 변경은 전역 undo와 자동 저장, 대형 문서의 lazy loading 위에서 동작해야 합니다. 이 단계에서는 한 줄을 넘기는 알고리즘부터 수백 페이지의 상태를 보존하는 방식까지 하나의 composition engine으로 묶었습니다.Page와 Flex책의 페이지는 절대 좌표만으로 구성되지 않습니다. 이미지와 설명이 나란히 놓이거나 여러 항목이 일정한 간격으로 이어지고, 폭이 줄면 다음 줄로 넘어가야 하는 영역도 있습니다. 그래서 일반 SVG 오브젝트와 별도로 CSS Flexbox를 내부 배치 엔진으로 사용하는 컨테이너를 만들었습니다. 바깥에서는 페이지 위의 `foreignObject` 하나지만, 안쪽에서는 row와 column, wrap, justify, align, gap과 padding이 자식 배치를 결정합니다.Flex 설정은 브라우저의 computed style만 믿지 않고 모두 직렬화 가능한 상태로 남겼습니다. 방향과 줄바꿈, 주축·교차축 정렬, 행과 열 간격, 네 방향 padding, fixed·hug·fill 크기 모드를 attribute에 기록하고 복원 시 이 값을 기준으로 실제 style을 다시 만듭니다. 화면에서 우연히 계산된 폭이 프로젝트의 원본 상태가 되지 않도록 편집값과 결과값을 구분했습니다.컨테이너와 자식의 selection도 별도로 관리했습니다. 바깥 프레임을 선택하면 일반 page object처럼 이동하고 회전하거나 크기를 바꿉니다. 안쪽 자식을 선택하면 grow, shrink, basis, order, align-self와 개별 width·height를 편집합니다. 텍스트 자식은 별도의 contentEditable 상태로 들어가며, 편집을 끝낼 때 HTML과 Flex item 상태를 함께 확정합니다.자식 순서를 drag로 바꿀 때는 pointermove마다 DOM을 계속 교체하지 않았습니다. 현재 마우스와 가장 가까운 삽입 경계를 계산해 marker만 움직이고, drop 시점에 DOM 순서를 한 번 바꿨습니다. layout invalidation을 줄이면서도 row, column과 wrap 상태에 맞는 시각적 순서를 유지했습니다. fixed 페이지 안에서도 내용의 관계를 좌표가 아니라 구조로 표현할 수 있게 되었습니다.자유 배치와 흐름의 왕복이미 페이지에 놓여 있는 텍스트와 이미지를 Flex 안으로 넣을 때는 단순히 DOM node를 옮길 수 없습니다. SVG image와 HTML child는 자산을 가리키는 방식부터 다르고, page local 좌표는 Flex formatting context 안에서 의미가 없습니다. 저는 선택된 page object를 자식용 HTML로 변환하면서 보이는 크기와 스타일, blob id, 이미지 filter, 텍스트와 문단 속성을 함께 옮겼습니다.반대로 자식을 컨테이너 밖으로 꺼낼 때는 현재 화면에 계산된 `getBoundingClientRect()`를 그대로 저장하지 않았습니다. 컨테이너와 page의 root transform, pan과 zoom을 제거해 실제 page local bounds를 구한 뒤 텍스트는 `foreignObject`, 이미지는 SVG image로 다시 만들었습니다. Flex가 계산한 최종 크기는 자유 배치 오브젝트의 width와 height가 되고, 화면에서 보이던 위치를 페이지 기준 x와 y로 환산합니다.이 왕복에서는 자산 생명주기도 보존했습니다. 자식 이미지가 참조하던 영구 blob id와 원본 편집 프로젝트 id를 새 SVG image에 다시 연결하고, 텍스트의 font family와 weight, paragraph style id도 잃지 않게 했습니다. 변환 도중 새 DOM이 완성되기 전에 기존 오브젝트를 지우지 않아 중간 실패가 원본 삭제로 이어지지 않도록 했습니다.자유 배치와 Flex를 함께 둔 이유는 편집 편의만이 아니었습니다. 나중에 fixed page를 reflow HTML로 변환할 때 같은 행에 놓인 이미지와 설명이 실제 row 구조를 가지고 있다면 시각적 관계를 훨씬 정확하게 옮길 수 있습니다. 페이지 안에서 좌표와 흐름을 왕복할 수 있게 하면서 종이 조판과 반응형 문서 사이에 필요한 구조가 만들어졌습니다.Text thread본문 조판은 텍스트 프레임을 하나의 방향 그래프로 연결하는 것에서 시작했습니다. 각 프레임에 안정적인 id를 주고 `next`와 `prev`를 서로 대칭으로 기록했습니다. 새 연결을 만들 때는 양쪽에 남아 있는 기존 포인터를 먼저 정리하고, 다음 포인터를 따라갔을 때 다시 출발 프레임에 도달하면 cycle로 판단해 연결을 거부했습니다. story는 분기 없는 선형 체인으로 유지했습니다.링크가 두 프레임에 중복 저장되기 때문에 snapshot 복원이나 오래된 프로젝트에서는 한쪽 참조만 남을 수 있습니다. 페이지를 불러온 뒤 전체 체인을 검사해 `A.next = B`인데 `B.prev`가 비어 있으면 반대 포인터를 채우고, 실제 대상이 사라졌다면 죽은 링크를 제거했습니다. 표시선만 고치는 것이 아니라 reflow가 따라갈 그래프 자체를 자가 복구하도록 했습니다.프레임을 삭제할 때도 story의 내용은 보존해야 합니다. 삭제되는 프레임의 문단을 앞 프레임 끝이나 다음 프레임 앞에 순서대로 옮기고, 양쪽 이웃을 직접 다시 연결했습니다. 프레임 하나가 없어져도 이전 프레임의 마지막 글자와 다음 프레임의 첫 글자가 같은 원고 순서를 유지합니다. 삭제 후에는 체인 전체를 한 번만 다시 흘려 중복 reflow를 피했습니다.페이지가 lazy 상태라 체인의 일부만 로드되어 있다면 그 조각만 보고 권위적인 재조판을 하지 않았습니다. head에서 tail까지 필요한 프레임이 모두 메모리에 있는지 확인하고, 완전한 체인이 만들어졌을 때만 전체 reflow를 실행했습니다. 여러 페이지가 연속으로 로드되는 동안에는 같은 head id를 pending set에 모아 체인당 한 번만 계산했습니다. 페이지 수가 늘어날 때 발생할 수 있는 반복 조판을 선형 작업으로 정리했습니다.줄 경계의 재계산프레임의 넘침은 editable element의 실제 `scrollHeight`와 사용 가능한 높이를 비교해 판정했습니다. 마지막 문단이 통째로 다음 프레임으로 들어가야 한다면 DOM node 자체를 이동합니다. 이 방식은 링크와 문자 스타일 span, 문단의 data attribute를 다시 만들지 않고 그대로 보존합니다. 하지만 긴 문단 하나가 프레임 높이보다 크면 문단 단위 이동만으로는 앞 페이지 하단에 큰 빈 공간이 남습니다.그래서 한 문단 안에서 현재 프레임에 들어가는 마지막 문자 offset을 찾았습니다. 단락의 전체 문자 길이가 `L`일 때 중간 offset에서 뒷부분을 잠시 제외한 상태의 높이를 측정합니다. 남은 앞부분이 프레임 안에 들어가면 탐색 구간을 뒤쪽 절반으로 옮기고, 넘치면 앞쪽 절반으로 줄입니다. 한 글자씩 검사하는 대신 대략 `O(log L)`회의 layout 측정으로 분할 위치를 구했습니다.문자 offset으로 DOM을 자를 때는 `textContent`를 잘라 새 문자열로 만들지 않았습니다. text node와 inline span을 순회하며 경계에 걸린 node만 분할하고, 뒤쪽 node들을 새 문단으로 이동했습니다. 굵기, 기울임, 링크, 문자 스타일처럼 문단 안에 중첩된 markup이 분할 전후에도 그대로 남습니다. 글자 수 계산에서는 `
`과 빈 placeholder가 실제 원고 문자로 잘못 포함되지 않도록 별도로 정규화했습니다.
분할된 두 문단에는 같은 group id를 남겼습니다. 다음 reflow가 시작되면 story 순서대로 같은 group의 조각을 먼저 하나의 원래 문단으로 합친 뒤 현재 폭과 스타일을 기준으로 다시 자릅니다. 이전 결과를 조금씩 수정하면 판형이나 자간을 여러 번 바꿀수록 조각과 빈 문단이 누적될 수 있습니다. 매번 원문 상태로 복원하고 다시 계산하는 방식으로 조판을 멱등적으로 만들었습니다.Pull과 push텍스트 흐름은 뒤로 보내는 push만으로 완성되지 않습니다. 앞 프레임에서 문장을 삭제하거나 글자 크기를 줄이면 뒤 프레임의 내용이 다시 앞으로 당겨져야 합니다. 각 프레임을 head부터 순회하면서 먼저 다음 프레임의 첫 문단을 현재 프레임 끝으로 가져오고, 넘치지 않는 동안 이 작업을 반복했습니다. 그다음 현재 프레임이 넘치면 마지막 문단을 뒤로 보내는 push를 실행했습니다.pull에서도 문단 전체가 들어가지 않는다고 즉시 되돌리면 프레임 아래에 한두 줄만큼의 빈 공간이 남습니다. 다음 문단 전체는 못 들어가도 앞부분 몇 줄은 들어갈 수 있기 때문입니다. push와 같은 분할 탐색을 pull에도 적용해 들어가는 앞부분만 현재 프레임에 남기고 나머지 tail을 다음 프레임의 선두로 돌려보냈습니다. 내용이 늘 때와 줄 때가 같은 line-fitting 규칙을 사용하게 했습니다.각 반복문에는 비정상 DOM이나 극단적인 입력에서 무한 순환하지 않도록 guard를 두었습니다. 프레임 하나에 글자 한 개도 들어가지 않거나 마지막 단락 하나만 남은 경우에는 더 이상 이동하지 않고 overset 상태로 남깁니다. 빈 프레임에는 편집 진입을 위한 최소 문단을 넣되 transient placeholder로 표시해 다음 reflow가 실제 원고로 끌어가지 않게 했습니다.스타일은 측정보다 먼저 확정했습니다. font family와 weight, font size, line-height, letter-spacing, 문단 여백이 적용되지 않은 상태에서 높이를 재면 다음 paint 이후 결과가 바뀝니다. 체인 전체에 최신 스타일을 재적용하고 실제 폰트가 준비된 상태에서 pull과 push를 수행했습니다. 같은 입력과 같은 프레임 크기에서는 언제 실행해도 같은 문단 경계가 나오도록 만들었습니다.문단의 연속성프레임을 넘어간 문단의 뒷부분은 새로운 문단이 아닙니다. 첫 줄 들여쓰기와 문단 앞 여백, drop cap을 다시 적용하면 페이지가 바뀌는 곳마다 같은 문단이 새로 시작한 것처럼 보입니다. 같은 split group에서 처음 등장한 조각만 head로 보고, 두 번째 이후 조각에는 continuation marker를 주어 들여쓰기와 앞 여백을 0으로 만들었습니다.이 규칙은 분할 함수 하나에만 맡기지 않았습니다. 과거 저장 데이터나 다른 importer가 만든 체인에는 marker가 누락되거나 연결이 끊긴 뒤 오래된 marker가 남을 수 있습니다. reflow 마지막 단계에서 chain 순서와 split group을 다시 읽어 continuation 상태를 정규화했습니다. 더 이상 thread에 연결되지 않은 독립 프레임에서는 stale marker를 제거해 일반 문단의 들여쓰기가 다시 보이도록 했습니다.우선순위도 분리했습니다. 문단에 직접 적용한 들여쓰기, 텍스트 프레임 전체에 적용한 override, 가져온 문서에 구워진 기본 스타일이 서로 덮어쓰지 않도록 명시적인 순서를 두었습니다. continuation 조각은 최종적으로 0이 이기지만 head 문단은 사용자가 가장 가까운 범위에 지정한 값을 사용합니다. 조판기가 문단 스타일 편집을 실행할 때마다 되돌리는 일을 막았습니다.브라우저의 SVG `foreignObject`는 내부 HTML이 바뀌어도 즉시 다시 그려지지 않는 경우가 있었습니다. reflow가 끝난 프레임에는 layout을 거의 건드리지 않는 가벼운 repaint를 주되, 현재 편집 중인 프레임은 focus가 풀릴 수 있으므로 제외했습니다. 실제 DOM 분배와 화면 paint가 같은 결과를 보여주도록 마지막 표시 단계까지 정리했습니다.Caret과 IME사용자가 타이핑하는 중에도 reflow가 실행되어야 하지만 현재 caret이 든 문단을 바로 다른 프레임으로 옮기면 입력 위치가 사라집니다. 입력 이벤트는 짧은 debounce로 모으고, reflow 전에 selection의 node와 offset, 현재 프레임 안의 절대 문자 offset을 함께 저장했습니다. 일반적인 경우에는 DOM node 자체를 이동하므로 기존 node 참조로 caret을 되돌릴 수 있습니다.분할 조각을 병합하고 다시 나누는 과정에서는 저장한 node가 제거될 수 있습니다. 이때는 reflow 전에 계산해 둔 문자 offset만큼 텍스트 노드를 순회해 새 node와 local offset을 찾았습니다. 원래 offset이 새 내용 길이보다 길면 마지막 문단 끝으로 보냈습니다. 조판 구조가 바뀌어도 사용자가 쓰던 논리적 글자 위치를 기준으로 편집을 계속할 수 있습니다.현재 caret이 속한 문단은 실시간 reflow 중에는 보호했습니다. 그 문단보다 뒤의 내용부터 먼저 흘려 보내고, 보호된 문단 자체는 편집 프레임에 남깁니다. 중간에 아주 긴 원고를 붙여 넣어 보호 문단 때문에 넘침이 갇힌 경우에는 입력을 확정한 뒤 보호를 풀고 전체 체인을 다시 계산합니다. 편집 안정성과 정확한 최종 조판을 두 단계로 나눈 방식입니다.한글 IME가 조합 중일 때 DOM을 재구성하면 아직 확정되지 않은 자모가 깨질 수 있습니다. `compositionstart`부터 `compositionend`까지 reflow를 보류하고, 조합이 끝난 뒤 최신 내용으로 다시 예약했습니다. 자동 페이지가 필요한 경우에도 먼저 blur로 조합을 확정하고 story의 절대 문자 위치를 저장한 다음 새 페이지를 만든 뒤 해당 위치로 편집 상태를 복원했습니다.자동 페이지네이션모든 텍스트 박스가 넘칠 때마다 페이지를 추가하도록 만들지는 않았습니다. 캡션이나 표 안의 라벨, 장식용 텍스트가 조금 넘쳤다고 새 페이지가 생기면 문서 구조가 망가집니다. 자동 흐름 대상은 페이지의 실제 마진 영역과 폭이 충분히 가깝고, 본문 높이의 대부분을 차지하는 프레임으로 제한했습니다. 기하학적으로 본문 프레임에 해당하는 경우에만 다음 페이지 생성을 허용했습니다.편집 중인 프레임과 chain tail을 먼저 기존 프레임들 안에서 재분배하고, 그래도 넘침이 남을 때만 tail 페이지 뒤에 새 페이지를 만듭니다. 새 페이지에는 같은 본문 마진을 가진 텍스트 프레임을 만들고 기존 tail과 연결합니다. 넘침이 계속되면 같은 과정을 반복하되 페이지 생성 기록은 하나의 batch로 묶었습니다.사용자가 story 끝에서 입력하고 있었다면 새 tail 프레임의 끝으로 편집 context를 옮겼습니다. 중간 문단을 수정하고 있었다면 저장해 둔 story 전체의 문자 offset을 기준으로 reflow 후 그 문자가 들어간 프레임을 찾아 caret을 복원했습니다. 페이지가 새로 생겼다는 사실보다 사용자가 같은 원고 위치에서 계속 쓰고 있다는 감각을 유지하는 것이 중요했습니다.자동 생성된 여러 페이지와 텍스트 분배는 하나의 사용자 동작으로 취급했습니다. undo 한 번에 생성된 페이지와 연결, 이동한 원고가 함께 돌아가고 redo에서도 같은 순서로 다시 만들어집니다. 페이지네이션이 편집 기능과 history 사이에서 따로 놀지 않도록 구조 변경과 콘텐츠 변경을 하나의 시간축에 올렸습니다.전역 history와 autosave멀티페이지 에디터에서 undo는 현재 페이지 stack만 되돌려서는 안 됩니다. 사용자가 3페이지를 고친 뒤 20페이지에서 작업했다면 마지막 명령은 20페이지의 작업입니다. 모든 history entry에 단조 증가하는 counter를 넣고, undo 시 각 페이지 stack의 맨 위 counter를 비교해 문서 전체에서 가장 최근 작업을 찾았습니다. 대상 페이지가 다르면 그 페이지로 이동한 뒤 상태를 복원합니다.오브젝트를 페이지 사이로 옮기는 작업은 출발 페이지와 도착 페이지를 동시에 변경합니다. 두 페이지의 이전 DOM을 같은 counter의 compound entry로 묶고, undo에서는 양쪽을 함께 복원한 뒤 출발 페이지를 보여줍니다. redo에서는 목적 페이지 상태를 다시 만들고 도착 페이지로 이동합니다. 페이지 생성과 삭제, 마스터 적용처럼 DOM snapshot만으로 설명할 수 없는 작업은 별도의 구조 delta로 같은 timeline에 합쳤습니다.판형 변경은 페이지별 snapshot보다 더 큰 작업입니다. 모든 페이지를 비우고 원고를 새 규격으로 다시 흘리므로 부분 DOM을 끼워 넣어 되돌리면 문서가 서로 다른 판형 상태를 가질 수 있습니다. 이전 판형과 다음 판형을 format history에 기록하고 undo·redo에서도 같은 reflow pipeline을 반대 규격으로 재실행하게 했습니다. 변환의 결과가 아니라 변환 자체를 재현하는 방식입니다.자동 저장은 사용자 action이 동기적으로 끝난 뒤 짧은 idle을 기다려 최신 DOM을 캡처했습니다. 연속 입력으로 debounce가 끝없이 갱신되는 starvation을 막기 위해 첫 예약부터의 max wait도 두었습니다. 저장이 이미 진행 중이면 두 번째 저장을 겹쳐 실행하지 않고 pending 표시만 남긴 다음, 첫 저장이 끝난 즉시 최신 상태를 한 번 더 저장했습니다. history와 영속 상태가 서로 다른 순간을 가리키지 않도록 저장 순서를 직렬화했습니다.대형 문서의 메모리페이지가 수백 장으로 늘어나면 모든 SVG와 이미지, editable HTML을 한꺼번에 메모리에 올리는 방식은 유지할 수 없습니다. 이 캔버스는 브라우저 scroll이 아니라 viewport group의 pan과 zoom으로 움직이기 때문에 일반적인 intersection observer만으로 보이는 페이지를 판단하기도 어렵습니다. 현재 transform과 페이지 배치식을 사용해 각 page box의 screen bounds를 직접 계산했습니다.뷰포트와 교차하는 페이지와 인접한 페이지를 우선 로드하고 나머지는 크기만 가진 skeleton으로 남겼습니다. pan이나 zoom 중에는 무거운 DOM 복원을 시작하지 않고 움직임이 멈춘 뒤 idle queue에서 화면 중심에 가까운 페이지부터 처리했습니다. 아주 작게 축소해 수십 장이 한 화면에 들어와도 동시에 유지할 페이지 수에는 상한을 두었습니다.페이지를 메모리에서 내릴 때는 현재 DOM을 저장용 형태로 정규화한 문자열의 signature를 만들었습니다. 마지막으로 저장한 signature와 완전히 같을 때만 실제 DOM을 제거했습니다. dirty page, 현재 page, 로딩 중인 page, history 복원 중인 page와 thread chain에 포함된 page는 eviction 대상에서 제외했습니다. 텍스트 그래프의 일부를 내렸다가 불완전한 체인으로 재조판하는 것보다 메모리를 조금 더 쓰는 편이 안전했습니다.출력과 format reflow에서는 반대로 모든 페이지를 강제로 로드합니다. 편집 화면의 가상화는 화면 성능을 위한 정책일 뿐 문서 원본을 일부만 읽어도 된다는 뜻이 아닙니다. 전체 상태가 필요한 작업과 현재 화면만 필요한 작업을 명확히 나누고, 대량 load가 끝난 뒤 master 적용과 panel 갱신, thread reflow를 batch로 한 번씩 실행했습니다.조판 엔진 완성이 단계에서 레이아웃 에디터는 페이지 위에 글상자를 놓는 도구를 넘어섰습니다. 자유 배치 오브젝트와 Flex hierarchy가 같은 페이지에 공존하고, 텍스트 프레임은 안정적인 방향 그래프로 연결되었습니다. 한 문단이 프레임 경계를 넘으면 이분 탐색으로 실제 line fit을 찾고, DOM 안의 문자 스타일을 보존한 채 앞부분과 뒷부분을 나눴습니다.내용이 늘 때는 push하고 줄 때는 pull하며, 매 reflow 전에 과거 조각을 병합해 같은 입력에서 같은 결과를 만들었습니다. continuation 문단의 들여쓰기와 여백, caret의 문자 위치, 한글 IME 조합 상태까지 조판 과정 안에서 보존했습니다. 본문 프레임의 넘침은 새 페이지와 다음 프레임을 만들고, 그 구조 변경은 전역 history와 자동 저장에 함께 기록됩니다.짧은 문단과 매우 긴 문단, 중첩 문자 스타일, 링크, 빈 문단, 중간 붙여넣기, 글꼴·행간·자간 변경, 페이지 삭제와 재연결, 수백 페이지 lazy load를 조합해 반복 검수했습니다. undo와 redo, 저장 중 재입력, 페이지 자동 생성과 제거까지 같은 원고 순서를 유지하는 것을 모두 확인했습니다. 원고가 페이지를 건너가면서도 편집 위치와 문단 의미를 잃지 않는 조판 엔진은 이 단계에서 완전히 구현되고 안정화되었습니다.
이전글목록으로다음글
저작권 고시Copyright Notice본 웹사이트의 모든 디자인 결과물 및 영상에 대한 저작권은 Abstract Cloud에 있으며, 저작권법 및 관련 법령에 의해 보호받습니다. 웹, 영상, 본문, 표지, 내지 디자인을 포함한 모든 콘텐츠는 저작권자의 자산으로, 사전 동의 없이 무단 복제, 배포, 2차 저작물 제작, 온라인 공유 등을 금지합니다. 이를 위반할 시, 저작권법에 따라 민형사상 책임을 질 수 있습니다. 정당한 구매와 저작권 보호는 창작자의 권리를 지키며, 더 나은 작품으로 보답할 힘이 됩니다.저작권자: Abstract Cloud | 대표자: 배창규(uragen)
© Abstract Cloud. All Rights Reserved.
HOMEFAQ이용 약관개인정보 이용방침help@opkle.app
010-2747-3403
상호 :추상적 형상 디자인(Abstract cloud)  |  대표자 :배창규사업자등록번호 :249-74-00533통신판매업 신고번호 :2025-의정부송산-0634주소 :경기도 의정부시 부용로 49, 108동 402호웹의 모든 콘텐츠, 디자인, 소스 코드에 대한
저작권은 Opkle에게 있습니다.
img default description