feat(project): 3단계 구조 프론트엔드 — 세션 관리·스코프·하위업무·멀티호밍·이미지
지난 커밋(07f4d0f)의 백엔드 위에 화면을 얹는다.
- 세션(기존 "단계") 문구 통일 + 프로젝트 스코프("이 프로젝트")일 때 보드
컬럼에 관리 UI 추가: 이름변경/완료토글/삭제(만든 사람만)/"+ 세션 추가"/
드래그 재정렬(카드 드래그와 같은 네이티브 HTML5 DnD, 기존
PUT stages/order 재사용).
- 프로젝트 화면 툴바에 "이 프로젝트"/"전체 프로젝트" 스코프 토글 추가.
기본은 아사나처럼 지금 프로젝트만(달력/타임라인/보드/리스트 4개 뷰
전부), "전체"는 예전처럼 접근 가능한 프로젝트를 다 합쳐서 보여준다.
서버가 tasks 각 행에 context_project_id(어느 프로젝트를 보다가 담겼는지)
를 실어주고 클라이언트가 그걸로 순수 필터링(재조회 없음).
- 하위 업무 인라인 UI — 업무 모달 안에 체크박스·제목·담당자·마감일이 한
줄인 하위업무 목록(변경 즉시 저장), "+ 하위 업무 추가". 카드에는
"완료/전체" 진행 배지만 표시. update_task 에 completed 필드 추가(세션이
없는 하위업무의 직접 완료 토글용).
- 멀티호밍 UI — 업무 모달에 "연결된 프로젝트" 칩 + "+ 프로젝트에 추가"(
프로젝트/세션 선택). 보드에서 다른 프로젝트가 원 소속인 카드는 그
프로젝트 배지를 달고 나타나며, 드래그로 세션을 옮기면
POST /api/tasks/{id}/links 로 그 프로젝트 안에서의 배치만 바뀐다(원본
불변). 링크 생성 권한을 다듬음 — 최초 연결은 원/대상 프로젝트 양쪽 권한이
필요하지만, 이미 연결된 카드를 그 프로젝트 보드 안에서 옮기는 것은 대상
프로젝트 권한만 있으면 되게 해 정상적인 드래그가 403 나지 않게 했다.
- 업무 설명란을 textarea → contenteditable 로 교체. 클립보드 이미지
붙여넣기/파일 업로드 버튼으로 삽입(기존 task_attachments 재사용, 새
GET /api/attachments/{id}/inline 추가 — 기존 /download 는
Content-Disposition: attachment 라 <img> 렌더링에 못 씀). 새 업무 작성
중(저장 전)에는 이미지 삽입 비활성화.
- .pj-modal-card 에 max-height/overflow-y 추가(내용이 늘어 화면보다
길어지는 것 방지).
홈(index.html)·프로젝트(project.html) 두 업무 모달 모두 요소 id 를 공유하는
기존 구조를 그대로 따라, 새 로직은 공용 함수(initDescEditor/initSubtaskBlock/
initLinksBlock)로 한 번만 작성해 양쪽에서 재사용한다.
docs/PROJECT_MODULE.md 갱신 — 1-1절에 3단계 구조·권한 변경 전반을 정리.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+55
-1
@@ -278,7 +278,7 @@
|
||||
/* ── 모달 ── */
|
||||
.pj-modal { position: fixed; inset: 0; background: rgba(20,22,26,.46); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||
.pj-modal[hidden] { display: none; }
|
||||
.pj-modal-card { background: #fff; border-radius: 16px; padding: 22px; width: min(480px, 92vw); box-shadow: 0 20px 60px rgba(0,0,0,.25); }
|
||||
.pj-modal-card { background: #fff; border-radius: 16px; padding: 22px; width: min(480px, 92vw); box-shadow: 0 20px 60px rgba(0,0,0,.25); max-height: 90vh; overflow-y: auto; }
|
||||
.pj-modal-card h3 { margin: 0 0 16px; font-size: 17px; }
|
||||
.pj-modal-hint { margin: -6px 0 14px; font-size: 12px; color: #6b7280; }
|
||||
.pj-modal-card label { display: block; font-size: 12.5px; font-weight: 600; color: #525860; margin-bottom: 12px; }
|
||||
@@ -389,6 +389,60 @@
|
||||
.pj-add-stage { width: 100%; padding: 10px; border: 1px dashed #c9cdd3; border-radius: 12px; background: #fff; color: #6b7280; font-size: 13px; font-weight: 600; cursor: pointer; }
|
||||
.pj-add-stage:hover { border-color: #1e1f21; color: #1e1f21; }
|
||||
|
||||
/* 세션(컬럼) 드래그 재정렬 */
|
||||
.pj-col-head[draggable="true"] { cursor: grab; }
|
||||
.pj-col.is-dragging { opacity: .45; }
|
||||
.pj-col.pj-col-drop-target { outline: 2px dashed #aebfe6; outline-offset: -2px; border-radius: 12px; }
|
||||
|
||||
/* 업무 카드 — 하위 업무 진행 배지("2/5") */
|
||||
.pj-card-subtasks { font-size: 11px; color: #6b7280; background: #eef0f2; border-radius: 999px; padding: 1px 7px; font-weight: 600; }
|
||||
|
||||
/* 프로젝트 스코프 토글(이 프로젝트 / 전체 프로젝트) — 뷰 탭과 같은 모양 재사용 */
|
||||
#pj-scope-toggle { margin-left: auto; }
|
||||
|
||||
/* ── 업무 설명 — contenteditable(이미지 붙여넣기/업로드 삽입) ── */
|
||||
.pj-desc-toolbar { display: flex; align-items: center; gap: 8px; margin: -6px 0 6px; }
|
||||
.pj-desc-toolbar .pj-muted-sm { font-size: 11px; color: #9aa1a9; }
|
||||
.pj-desc-editor {
|
||||
min-height: 64px; max-height: 260px; overflow-y: auto; padding: 8px 10px;
|
||||
border: 1px solid #d8dce2; border-radius: 8px; font-size: 13.5px; font-family: inherit;
|
||||
line-height: 1.5; margin-bottom: 12px; background: #fff;
|
||||
}
|
||||
.pj-desc-editor:empty::before { content: attr(data-placeholder); color: #b7bdc5; }
|
||||
.pj-desc-editor img { max-width: 100%; border-radius: 6px; margin: 4px 0; display: block; }
|
||||
.pj-desc-editor[contenteditable="false"] { background: #f6f7f8; color: #9aa1a9; }
|
||||
|
||||
/* ── 하위 업무(인라인) ── */
|
||||
.pj-subtask-list { list-style: none; margin: 0 0 8px; padding: 0; }
|
||||
.pj-subtask-row { display: flex; align-items: center; gap: 6px; padding: 4px 2px; }
|
||||
.pj-subtask-row.is-done .pj-subtask-title { text-decoration: line-through; color: #9aa1a9; }
|
||||
.pj-subtask-row input[type="checkbox"] { width: auto; margin: 0; flex: 0 0 auto; }
|
||||
.pj-subtask-title {
|
||||
/* flex-basis 는 반드시 0(퍼센트 아님) — auto 로 두면 .pj-modal-card 의 공용
|
||||
input[type=text] 규칙이 준 width:100% 로 넘어가 버려 한 줄 배치가 깨진다. */
|
||||
flex: 1 1 0; width: auto !important; min-width: 0;
|
||||
border: 1px solid transparent !important; background: transparent;
|
||||
padding: 4px 6px !important; font-size: 13px !important; margin: 0 !important;
|
||||
}
|
||||
.pj-subtask-title:hover, .pj-subtask-title:focus { border-color: #d8dce2 !important; background: #fff; }
|
||||
.pj-subtask-assignee {
|
||||
flex: 0 0 auto; width: 110px !important; font-size: 12px !important; padding: 4px 6px !important; margin: 0 !important;
|
||||
}
|
||||
.pj-subtask-due { flex: 0 0 auto; width: 128px !important; font-size: 12px !important; padding: 4px 6px !important; margin: 0 !important; }
|
||||
.pj-subtask-form { display: flex; gap: 6px; }
|
||||
.pj-subtask-form input { flex: 1; padding: 7px 10px; border: 1px solid #d8dce2; border-radius: 8px; font-size: 13px; font-family: inherit; }
|
||||
|
||||
/* ── 연결된 프로젝트(멀티호밍) 칩 ── */
|
||||
.pj-link-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
||||
.pj-link-chip {
|
||||
display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600;
|
||||
padding: 3px 6px 3px 10px; border-radius: 999px; background: #eef0f2; color: #333;
|
||||
}
|
||||
.pj-link-chip .pj-link-del { border: none; background: transparent; cursor: pointer; color: #9aa1a9; font-size: 13px; line-height: 1; padding: 2px; }
|
||||
.pj-link-chip .pj-link-del:hover { color: #c22b10; }
|
||||
.pj-link-form { display: flex; gap: 6px; align-items: center; }
|
||||
.pj-link-form select { flex: 1; padding: 7px 8px; border: 1px solid #d8dce2; border-radius: 8px; font-size: 13px; font-family: inherit; }
|
||||
|
||||
/* ── 알림센터 ── */
|
||||
.pj-inbox { list-style: none; margin: 0; padding: 0; }
|
||||
.pj-inbox-item { display: flex; align-items: flex-start; gap: 12px; padding: 14px 12px; border: 1px solid #eef0f2; border-radius: 12px; margin-bottom: 8px; cursor: pointer; background: #fff; }
|
||||
|
||||
Reference in New Issue
Block a user