<script setup lang="ts">
// 업무 작성/수정 — 제목/내용/체크리스트 + 사이드(담당자/마감/지시자) · 백엔드 연동
// taskId 파라미터가 있으면 '수정 모드'로 동작(기존 값 미리 채움 + PATCH 저장)
import { computed, defineComponent, h, ref, watch } from 'vue'
import { useRoute, useRouter, RouterLink } from 'vue-router'
import AppShell from '@/layouts/AppShell.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import { useProjectStore } from '@/stores/project.store'
import { useMemberStore } from '@/stores/member.store'
import { useTaskStore } from '@/stores/task.store'
import { useAuthStore } from '@/stores/auth.store'
import { useTask } from '@/composables/useTask'
import { useAi } from '@/composables/useAi'
import { useDialog } from '@/composables/useDialog'
import { attachmentKindOf, fileBadge, formatBytes } from '@/shared/utils/format'
import { CHECKLIST_CATEGORIES } from '@/mock/relay.mock'
import type {
  Project,
  ChecklistItem,
  ChecklistCategory,
  User,
} from '@/mock/relay.mock'
import type { ApiMember } from '@/types/repo'
import type { ApiAttachment } from '@/types/task'
import type { ChecklistSuggestionGroup } from '@/types/ai'

const route = useRoute()
const router = useRouter()
const projectId = computed(() => String(route.params.projectId))
// 수정 모드 여부 — taskId 파라미터 존재 시 편집
const taskId = computed(() => (route.params.taskId ? Number(route.params.taskId) : null))
const isEdit = computed(() => taskId.value !== null)

const projectStore = useProjectStore()
const memberStore = useMemberStore()
const taskStore = useTaskStore()
const authStore = useAuthStore()
const taskApi = useTask()
const ai = useAi()
const dialog = useDialog()

// API 멤버 → 화면용 User(이니셜/색상 파생)
function toUserView(m: ApiMember): User {
  return {
    id: m.id,
    name: m.name,
    role: m.role ?? '',
    avatarUrl: m.avatarUrl,
  }
}

// ISO → input[type=date] 용 'yyyy-MM-dd'
function toDateInput(iso: string): string {
  const d = new Date(iso)
  if (Number.isNaN(d.getTime())) return ''
  const y = d.getFullYear()
  const m = String(d.getMonth() + 1).padStart(2, '0')
  const day = String(d.getDate()).padStart(2, '0')
  return `${y}-${m}-${day}`
}

// 프로젝트 헤더(빵부스러기) — API 연동
const repo = ref<Project | null>(null)
async function loadProject() {
  try {
    repo.value = await projectStore.fetchOne(projectId.value)
    // 업무 작성/수정은 프로젝트 관리자만 가능 — 일반 멤버는 진입 즉시 상세로 돌려보낸다
    // (백엔드도 admin 전용이지만, 폼 화면 자체를 열지 못하게 막는다)
    if (repo.value && !repo.value.canManage) {
      void dialog.alert('업무 작성은 프로젝트 관리자만 가능합니다.', { variant: 'warning' })
      await router.replace(`/projects/${projectId.value}`)
    }
  } catch {
    repo.value = null
  }
}
// 담당자 후보 = 프로젝트 멤버
async function loadMembers() {
  try {
    await memberStore.fetchList(projectId.value)
  } catch {
    // 인터셉터가 알림 처리
  }
}
// 수정 모드: 기존 업무 값을 폼에 미리 채운다
async function loadTaskForEdit() {
  if (taskId.value === null) return
  try {
    const t = await taskApi.get(projectId.value, taskId.value)
    title.value = t.title
    contentText.value = t.content.join('\n')
    dueDate.value = t.dueDate ? toDateInput(t.dueDate) : ''
    assignees.value = t.assignees.map(toUserView)
    // id 를 보존해 저장 시 기존 항목을 식별(완료 상태 유지)
    checklist.value = t.checklist.map((c) => ({
      id: c.id,
      text: c.text,
      done: c.done,
      category: c.category,
    }))
    existingFiles.value = t.files
    checkpoints.value = t.checkpoints.map((c) => toDateInput(c.checkAt)).sort()
  } catch {
    // 404 등은 인터셉터가 알림 처리
  }
}
watch(
  [projectId, taskId],
  () => {
    loadProject()
    loadMembers()
    loadTaskForEdit()
  },
  { immediate: true },
)

// 제목 / 내용
const title = ref('')
const contentText = ref('')

// 지시자 = 현재 로그인 사용자
const issuer = computed<User>(() => {
  const u = authStore.user
  return {
    id: u?.id ?? '',
    name: u?.name ?? '나',
    role: u?.role ?? '',
    avatarUrl: u?.avatarUrl ?? null,
  }
})

// --- 담당자 선택 (프로젝트 멤버 중) ---
const assignees = ref<User[]>([])
const assigneeQuery = ref('')
const showAssigneeDropdown = ref(false)
function removeAssignee(id: string) {
  assignees.value = assignees.value.filter((a) => a.id !== id)
}
// 아직 선택되지 않은 멤버 후보(이름 부분일치)
const assigneeCandidates = computed(() => {
  const selected = new Set(assignees.value.map((a) => a.id))
  const kw = assigneeQuery.value.trim()
  return memberStore.members
    .filter((m) => !selected.has(m.user.id))
    .filter((m) => !kw || m.user.name.includes(kw) || m.email.includes(kw))
})
function addAssignee(userId: string) {
  const m = memberStore.members.find((x) => x.user.id === userId)
  if (m && !assignees.value.some((a) => a.id === userId)) {
    assignees.value.push(m.user)
  }
  assigneeQuery.value = ''
  showAssigneeDropdown.value = false
}
function onAssigneeBlur() {
  // 항목은 @mousedown.prevent 로 선택하므로 약간 지연 후 닫는다
  setTimeout(() => (showAssigneeDropdown.value = false), 120)
}

// --- 마감기한 ---
const dueDate = ref('') // yyyy-MM-dd (input[type=date])

// --- 중간 점검일(여러 개) — 작업자가 진행 상태를 보고하는 기준일 ---
const checkpoints = ref<string[]>([]) // yyyy-MM-dd 목록
const newCheckpoint = ref('')
function addCheckpoint() {
  const d = newCheckpoint.value
  if (!d) return
  if (!checkpoints.value.includes(d)) {
    checkpoints.value.push(d)
    checkpoints.value.sort()
  }
  newCheckpoint.value = ''
}
function removeCheckpoint(d: string) {
  checkpoints.value = checkpoints.value.filter((x) => x !== d)
}

// 체크리스트 (생성 시 항목만 등록 — 완료여부는 항상 false 로 생성)
// 항목은 카테고리(탭: 필수/관리/보안/부가)별로 추가한다.
const checklist = ref<ChecklistItem[]>([])
const newItem = ref('')
const activeCategory = ref<ChecklistCategory>('필수')
const doneCount = computed(() => checklist.value.filter((c) => c.done).length)
const progressPct = computed(() =>
  checklist.value.length ? Math.round((doneCount.value / checklist.value.length) * 100) : 0,
)
// 활성 탭의 항목 / 카테고리별 개수
const categoryItems = computed(() =>
  checklist.value.filter((c) => c.category === activeCategory.value),
)
function categoryCount(cat: ChecklistCategory): number {
  return checklist.value.filter((c) => c.category === cat).length
}
// 인덱스가 아니라 항목 참조로 제거(탭 필터로 인덱스가 어긋나므로)
function removeItem(item: ChecklistItem) {
  checklist.value = checklist.value.filter((c) => c !== item)
}
function addItem() {
  const text = newItem.value.trim()
  if (!text) return
  checklist.value.push({ text, done: false, category: activeCategory.value })
  newItem.value = ''
}

// --- AI 할 일 추천 ---
const aiSuggesting = ref(false)
const aiSuggestions = ref<ChecklistSuggestionGroup[] | null>(null)
const aiError = ref<string | null>(null)

// 프로젝트 설명 + 제목/내용을 분석해 카테고리별 할 일 추천을 받는다
async function requestAiSuggestions() {
  if (aiSuggesting.value) return
  // AI 추천은 제목·내용 기반 — 비면 해당 필드에 인라인 에러로 안내
  titleError.value = title.value.trim().length === 0
  contentError.value = contentText.value.trim().length === 0
  if (titleError.value || contentError.value) return
  aiSuggesting.value = true
  aiError.value = null
  aiSuggestions.value = null
  selectedSuggestions.value = new Set()
  try {
    const content = contentText.value
      .split('\n')
      .map((p) => p.trim())
      .filter((p) => p.length > 0)
    const { groups } = await ai.suggestChecklist({
      projectId: projectId.value,
      title: title.value.trim(),
      content: content.length ? content : undefined,
    })
    aiSuggestions.value = groups
  } catch {
    aiError.value = '추천을 가져오지 못했습니다. 잠시 후 다시 시도해 주세요.'
  } finally {
    aiSuggesting.value = false
  }
}

// --- 추천 선택 모델: 항목을 골라(selected) '업무에 추가' 시 일괄 반영 ---
const selectedSuggestions = ref<Set<string>>(new Set())

// 이미 체크리스트에 있는 항목(중복 방지 — 잠금 표시)
function isSuggestionAdded(text: string): boolean {
  return checklist.value.some((c) => c.text === text)
}
function isSelected(text: string): boolean {
  return selectedSuggestions.value.has(text)
}
// 항목 토글(이미 추가된 항목은 잠금)
function toggleSuggestion(text: string) {
  if (isSuggestionAdded(text)) return
  const next = new Set(selectedSuggestions.value)
  if (next.has(text)) next.delete(text)
  else next.add(text)
  selectedSuggestions.value = next
}
// 그룹에서 아직 추가되지 않은(선택 가능한) 항목
function selectableItems(group: ChecklistSuggestionGroup): string[] {
  return group.items.filter((t) => !isSuggestionAdded(t))
}
function isGroupAllSelected(group: ChecklistSuggestionGroup): boolean {
  const sel = selectableItems(group)
  return sel.length > 0 && sel.every((t) => isSelected(t))
}
function toggleGroup(group: ChecklistSuggestionGroup) {
  const sel = selectableItems(group)
  const makeAll = !isGroupAllSelected(group)
  const next = new Set(selectedSuggestions.value)
  sel.forEach((t) => (makeAll ? next.add(t) : next.delete(t)))
  selectedSuggestions.value = next
}
const selectedCount = computed(() => selectedSuggestions.value.size)
const suggestionTotal = computed(
  () => aiSuggestions.value?.reduce((s, g) => s + g.items.length, 0) ?? 0,
)
// 카테고리 → 섹션 스타일 클래스 (AI 전체명·체크리스트 단축명 모두 지원)
function sectionClass(category: string): string {
  const map: Record<string, string> = {
    '필수 기능': 'sec-1',
    필수: 'sec-1',
    '관리 기능': 'sec-2',
    관리: 'sec-2',
    '보안 기능': 'sec-3',
    보안: 'sec-3',
    '부가 기능': 'sec-4',
    부가: 'sec-4',
  }
  return map[category] ?? 'sec-1'
}
// AI 카테고리("필수 기능") → 체크리스트 카테고리("필수")
function aiCategoryToChecklist(aiCat: string): ChecklistCategory {
  const short = aiCat.replace(/\s*기능$/, '').trim()
  return (CHECKLIST_CATEGORIES as string[]).includes(short)
    ? (short as ChecklistCategory)
    : '필수'
}
// 선택 항목을 (AI 카테고리 → 체크리스트 카테고리) 매핑해 일괄 추가 후 모달 닫기
function commitSelected() {
  if (selectedCount.value === 0 || !aiSuggestions.value) return
  for (const group of aiSuggestions.value) {
    const cat = aiCategoryToChecklist(group.category)
    for (const item of group.items) {
      if (isSelected(item)) addSuggestion(item, cat)
    }
  }
  dismissSuggestions()
}
function addSuggestion(text: string, category: ChecklistCategory = '필수') {
  if (isSuggestionAdded(text)) return
  checklist.value.push({ text, done: false, category })
}
function dismissSuggestions() {
  aiSuggestions.value = null
  aiError.value = null
  selectedSuggestions.value = new Set()
}

// 섹션 아이콘(카테고리별) — 다중 path 대응 위해 innerHTML 렌더
const SEC_ICONS: Record<string, string> = {
  '필수 기능': '<path d="m5 12 5 5L20 7"/>',
  '관리 기능': '<path d="M20 7H4M20 12H4M20 17H4"/>',
  '보안 기능': '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
  '부가 기능':
    '<path d="M9 18h6M10 22h4"/><path d="M12 2a7 7 0 0 0-4 12.6c.6.5 1 1.2 1 2v.4h6v-.4c0-.8.4-1.5 1-2A7 7 0 0 0 12 2z"/>',
}
const SecIcon = defineComponent({
  props: { cat: { type: String, required: true } },
  setup(props) {
    return () =>
      h('svg', {
        viewBox: '0 0 24 24',
        fill: 'none',
        stroke: 'currentColor',
        'stroke-width': '2',
        'stroke-linecap': 'round',
        'stroke-linejoin': 'round',
        innerHTML: SEC_ICONS[props.cat] ?? '',
      })
  },
})

// --- 첨부파일 ---
// 생성 화면엔 업무가 아직 없어 바로 업로드 못 함 → 선택한 파일을 들고 있다가 제출 시 업로드.
// 수정 모드에선 기존 첨부(existingFiles)도 표시하고, 새 파일은 저장 시 업로드한다.
const MAX_FILE_SIZE = 25 * 1024 * 1024 // 25MB (백엔드 제한과 일치)
const existingFiles = ref<ApiAttachment[]>([])
const pendingFiles = ref<File[]>([])
const fileInput = ref<HTMLInputElement | null>(null)
const totalFileCount = computed(
  () => existingFiles.value.length + pendingFiles.value.length,
)
function fileBadgeFor(name: string) {
  return fileBadge(attachmentKindOf(name))
}
function triggerFilePicker() {
  fileInput.value?.click()
}
function onPickFiles(e: Event) {
  const input = e.target as HTMLInputElement
  const picked = input.files ? Array.from(input.files) : []
  const ok = picked.filter((f) => f.size <= MAX_FILE_SIZE)
  pendingFiles.value.push(...ok)
  if (ok.length < picked.length) {
    void dialog.alert(`25MB를 초과하는 파일 ${picked.length - ok.length}개는 제외했습니다.`, {
      variant: 'warning',
    })
  }
  input.value = '' // 같은 파일을 다시 선택할 수 있도록 초기화
}
function removePending(index: number) {
  pendingFiles.value.splice(index, 1)
}
// 수정 모드: 기존 첨부 즉시 삭제(API)
async function removeExisting(fileId: string) {
  if (taskId.value === null) return
  try {
    await taskApi.removeFile(projectId.value, taskId.value, fileId)
    existingFiles.value = existingFiles.value.filter((f) => f.id !== fileId)
  } catch {
    // 인터셉터가 알림 처리
  }
}
// 업무 생성/수정 후 대기 중인 파일들을 업로드(베스트에포트) → 실패 개수 반환
async function uploadPendingFiles(tid: number): Promise<number> {
  let failed = 0
  for (const file of pendingFiles.value) {
    try {
      await taskApi.uploadFile(projectId.value, tid, file)
    } catch {
      failed += 1
    }
  }
  return failed
}

// --- 제출 ---
const submitting = ref(false)
// 필드별 인라인 검증 — 제목·내용·담당자 필수. 입력하면 즉시 해제.
const titleError = ref(false)
const contentError = ref(false)
const assigneeError = ref(false)
watch(title, (v) => {
  if (v.trim().length > 0) titleError.value = false
})
watch(contentText, (v) => {
  if (v.trim().length > 0) contentError.value = false
})
watch(
  assignees,
  (v) => {
    if (v.length > 0) assigneeError.value = false
  },
  { deep: true },
)
function validateForm(): boolean {
  titleError.value = title.value.trim().length === 0
  contentError.value = contentText.value.trim().length === 0
  assigneeError.value = assignees.value.length === 0
  return !titleError.value && !contentError.value && !assigneeError.value
}

async function submitTask() {
  if (submitting.value) return
  if (!validateForm()) return
  submitting.value = true
  try {
    const content = contentText.value
      .split('\n')
      .map((p) => p.trim())
      .filter((p) => p.length > 0)
    const assigneeIds = assignees.value.map((a) => a.id)
    const dueIso = dueDate.value ? new Date(dueDate.value).toISOString() : null
    const checkpointIsos = checkpoints.value.map((d) => new Date(d).toISOString())

    let tid: number
    if (isEdit.value && taskId.value !== null) {
      // 수정 — 제목/내용/담당자/마감 + 체크리스트(추가/삭제/순서) 동기화
      const updated = await taskStore.update(projectId.value, taskId.value, {
        title: title.value.trim(),
        content,
        assigneeIds,
        dueDate: dueIso,
        checklist: checklist.value.map((c) =>
          c.id
            ? { id: c.id, text: c.text, category: c.category }
            : { text: c.text, category: c.category },
        ),
        checkpoints: checkpointIsos,
      })
      tid = updated.id
    } else {
      const created = await taskStore.create(projectId.value, {
        title: title.value.trim(),
        content: content.length ? content : undefined,
        assigneeIds,
        dueDate: dueIso ?? undefined,
        checklist: checklist.value.length
          ? checklist.value.map((c) => ({ text: c.text, category: c.category }))
          : undefined,
        checkpoints: checkpointIsos.length ? checkpointIsos : undefined,
      })
      tid = created.id
    }

    // 선택한 파일 업로드(업무가 생성/확정된 뒤에야 가능) — 베스트에포트
    const failed = await uploadPendingFiles(tid)
    if (failed > 0) {
      void dialog.alert(
        `파일 ${failed}개 업로드에 실패했습니다. 업무 상세 화면에서 다시 첨부해 주세요.`,
        { variant: 'warning' },
      )
    }
    router.push(`/projects/${projectId.value}/tasks/${tid}`)
  } catch {
    // 인터셉터가 알림 처리(권한/검증 등)
  } finally {
    submitting.value = false
  }
}

function goBack() {
  router.push(`/projects/${projectId.value}`)
}
</script>

<template>
  <AppShell>
    <div class="page">
      <div class="crumb">
        <RouterLink
          to="/projects"
          class="lnk"
        >
          프로젝트
        </RouterLink>
        <span class="sep">/</span>
        <RouterLink
          :to="`/projects/${projectId}`"
          class="lnk"
        >
          {{ repo?.name ?? '프로젝트' }}
        </RouterLink>
        <span class="sep">/</span>
        {{ isEdit ? '수정' : '새 업무' }}
      </div>

      <div class="pagehead">
        <h1>{{ isEdit ? '업무 수정' : '새 업무 작성' }}</h1>
        <div class="actions">
          <button
            class="btn ghost"
            type="button"
            @click="goBack"
          >
            취소
          </button>
          <button
            class="btn ai"
            type="button"
            :disabled="aiSuggesting"
            title="프로젝트·업무 내용을 분석해 할 일을 추천합니다"
            @click="requestAiSuggestions"
          >
            <svg
              width="15"
              height="15"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="2"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <path d="M12 3 13.4 8.6 19 10l-5.6 1.4L12 17l-1.4-5.6L5 10l5.6-1.4Z" />
              <path d="M18 16l.7 2.3L21 19l-2.3.7L18 22l-.7-2.3L15 19l2.3-.7Z" />
            </svg>
            {{ aiSuggesting ? '분석 중…' : 'AI 추천' }}
          </button>
          <button
            class="btn primary"
            type="button"
            :disabled="submitting"
            @click="submitTask"
          >
            <svg
              width="15"
              height="15"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="2.2"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <path d="m22 2-7 20-4-9-9-4Z" />
              <path d="M22 2 11 13" />
            </svg>
            {{ submitting ? (isEdit ? '저장 중…' : '전송 중…') : (isEdit ? '저장' : '지시 보내기') }}
          </button>
        </div>
      </div>

      <div class="layout">
        <!-- 메인 -->
        <section class="card main-card">
          <input
            v-model="title"
            class="title-input"
            :class="{ 'has-error': titleError }"
            placeholder="업무 제목을 입력하세요"
          >
          <div
            v-if="titleError"
            class="field-error"
          >
            업무 제목을 입력해 주세요.
          </div>
          <div class="divider" />

          <!-- 업무 내용 -->
          <div class="field">
            <div class="field-label">
              <span class="req">*</span> 업무 내용
            </div>
            <div
              class="editor"
              :class="{ 'has-error': contentError }"
            >
              <div class="editor-toolbar">
                <button
                  class="tb"
                  type="button"
                  style="font-weight: 800"
                >
                  B
                </button>
                <button
                  class="tb"
                  type="button"
                  style="font-style: italic; font-family: Georgia, serif"
                >
                  I
                </button>
                <button
                  class="tb"
                  type="button"
                  style="text-decoration: underline"
                >
                  U
                </button>
                <span class="tb-sep" />
                <button
                  class="tb"
                  type="button"
                  title="목록"
                >
                  <svg
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                  ><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" /></svg>
                </button>
                <button
                  class="tb"
                  type="button"
                  title="번호 목록"
                >
                  <svg
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                  ><path d="M10 6h11M10 12h11M10 18h11M4 6h1v4M4 10h2M6 18H4l2-3H4" /></svg>
                </button>
                <span class="tb-sep" />
                <button
                  class="tb"
                  type="button"
                  title="링크"
                >
                  <svg
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                  ><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1" /><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1" /></svg>
                </button>
                <button
                  class="tb"
                  type="button"
                  title="멘션"
                >
                  @
                </button>
                <button
                  class="tb"
                  type="button"
                  title="코드"
                >
                  <svg
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                  ><path d="m16 18 6-6-6-6M8 6l-6 6 6 6" /></svg>
                </button>
              </div>
              <textarea
                v-model="contentText"
                class="editor-body"
                placeholder="업무 배경과 요구사항을 작성하세요. (줄바꿈은 문단으로 저장됩니다)"
              />
            </div>
            <div
              v-if="contentError"
              class="field-error"
            >
              업무 내용을 입력해 주세요.
            </div>
          </div>

          <!-- 체크리스트 -->
          <div class="field">
            <div class="check-head">
              <div
                class="field-label"
                style="margin-bottom: 0"
              >
                해야 할 일
              </div>
              <div class="progress-wrap">
                <div class="progress-bar">
                  <i :style="{ width: progressPct + '%' }" />
                </div>
                <span class="progress-txt">{{ doneCount }} / {{ checklist.length }} 완료</span>
              </div>
            </div>
            <!-- 카테고리 탭 — 필수/관리/보안/부가 -->
            <div
              class="cat-tabs"
              role="tablist"
            >
              <button
                v-for="tab in CHECKLIST_CATEGORIES"
                :key="tab"
                type="button"
                class="cat-tab"
                :class="[sectionClass(tab), { active: activeCategory === tab }]"
                role="tab"
                :aria-selected="activeCategory === tab"
                @click="activeCategory = tab"
              >
                <span class="cat-dot" />
                {{ tab }}
                <span class="cat-count">{{ categoryCount(tab) }}</span>
              </button>
            </div>
            <div class="checklist">
              <div
                v-for="(item, index) in categoryItems"
                :key="index"
                class="check-item"
                :class="{ done: item.done }"
              >
                <span
                  class="cbox"
                  :class="{ done: item.done }"
                >
                  <svg
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="3.5"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                  ><path d="M20 6 9 17l-5-5" /></svg>
                </span>
                <span class="txt">{{ item.text }}</span>
                <span
                  class="row-del"
                  @click="removeItem(item)"
                >
                  <svg
                    width="15"
                    height="15"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                  ><path d="M18 6 6 18M6 6l12 12" /></svg>
                </span>
              </div>
              <div
                v-if="categoryItems.length === 0"
                class="check-empty"
              >
                '{{ activeCategory }}' 항목이 없습니다. 아래에서 추가하세요.
              </div>
            </div>
            <div class="add-row">
              <span class="plus">+</span>
              <input
                v-model="newItem"
                class="add-input"
                :placeholder="`'${activeCategory}' 항목 추가`"
                @keydown.enter="addItem"
              >
            </div>

            <!-- AI 추천 할 일 — 모달 -->
            <Teleport to="body">
              <div
                v-if="aiSuggesting || aiSuggestions || aiError"
                class="ai-overlay"
                @click.self="!aiSuggesting && dismissSuggestions()"
              >
                <div
                  class="ai-modal"
                  role="dialog"
                  aria-modal="true"
                  aria-label="AI 추천 할 일"
                >
                  <!-- 헤더 -->
                  <div class="ai-head">
                    <div class="ai-head-top">
                      <div class="ai-badge">
                        <svg
                          viewBox="0 0 24 24"
                          fill="currentColor"
                        >
                          <path d="M12 2.5l1.8 4.9 4.9 1.8-4.9 1.8L12 15.9l-1.8-4.9L5.3 9.2l4.9-1.8L12 2.5z" />
                          <path d="M19 14l.9 2.4 2.4.9-2.4.9L19 20.6l-.9-2.4-2.4-.9 2.4-.9L19 14z" />
                        </svg>
                      </div>
                      <div class="ai-htext">
                        <div class="ai-title">
                          AI 추천 할 일 <span class="auto">자동 생성</span>
                        </div>
                        <div class="ai-sub">
                          <template v-if="aiSuggesting">
                            프로젝트와 업무 내용을 분석하고 있어요…
                          </template>
                          <template v-else-if="aiError">
                            {{ aiError }}
                          </template>
                          <template v-else>
                            프로젝트 <b>{{ repo?.name ?? '' }}</b> 내용을 분석해 <b>{{ suggestionTotal }}개</b> 항목을 제안했어요. 필요한 항목만 골라 추가하세요.
                          </template>
                        </div>
                      </div>
                    </div>
                    <button
                      class="ai-x"
                      type="button"
                      aria-label="닫기"
                      :disabled="aiSuggesting"
                      @click="dismissSuggestions"
                    >
                      <svg
                        viewBox="0 0 24 24"
                        fill="none"
                        stroke="currentColor"
                        stroke-width="2"
                        stroke-linecap="round"
                        stroke-linejoin="round"
                      ><path d="M18 6 6 18M6 6l12 12" /></svg>
                    </button>
                  </div>

                  <!-- 본문 -->
                  <div class="ai-body">
                    <div
                      v-if="aiSuggesting"
                      class="ai-state"
                    >
                      잠시만 기다려 주세요…
                    </div>
                    <div
                      v-else-if="aiError"
                      class="ai-state err"
                    >
                      {{ aiError }}
                    </div>
                    <template v-else-if="aiSuggestions">
                      <section
                        v-for="group in aiSuggestions"
                        :key="group.category"
                        class="sec"
                        :class="sectionClass(group.category)"
                      >
                        <div class="sec-head">
                          <span class="sec-dot"><SecIcon :cat="group.category" /></span>
                          <span class="sec-name">{{ group.category }}</span>
                          <span class="sec-count">{{ group.items.length }}</span>
                          <button
                            class="sec-all"
                            :class="{ on: isGroupAllSelected(group) }"
                            type="button"
                            @click="toggleGroup(group)"
                          >
                            <svg
                              viewBox="0 0 24 24"
                              fill="none"
                              stroke="currentColor"
                              stroke-width="2.2"
                              stroke-linecap="round"
                              stroke-linejoin="round"
                            ><path :d="isGroupAllSelected(group) ? 'M5 12h14' : 'M5 12h14M12 5v14'" /></svg>
                            {{ isGroupAllSelected(group) ? '전체 해제' : '전체 추가' }}
                          </button>
                        </div>
                        <div class="items">
                          <button
                            v-for="(item, i) in group.items"
                            :key="i"
                            class="item"
                            :class="{ added: isSuggestionAdded(item) || isSelected(item) }"
                            type="button"
                            :disabled="isSuggestionAdded(item)"
                            @click="toggleSuggestion(item)"
                          >
                            <span class="item-check">
                              <svg
                                class="plus"
                                viewBox="0 0 24 24"
                                fill="none"
                                stroke="currentColor"
                                stroke-width="2.4"
                                stroke-linecap="round"
                                stroke-linejoin="round"
                              ><path d="M5 12h14M12 5v14" /></svg>
                              <svg
                                class="tick"
                                viewBox="0 0 24 24"
                                fill="none"
                                stroke="currentColor"
                                stroke-width="3"
                                stroke-linecap="round"
                                stroke-linejoin="round"
                              ><path d="M20 6 9 17l-5-5" /></svg>
                            </span>
                            <span class="item-text">{{ item }}</span>
                          </button>
                        </div>
                      </section>
                    </template>
                  </div>

                  <!-- 푸터 -->
                  <div
                    v-if="aiSuggestions"
                    class="ai-foot"
                  >
                    <div class="foot-info">
                      <div class="foot-count">
                        <span class="n">{{ selectedCount }}</span>개 선택됨
                      </div>
                      <div class="foot-hint">
                        필요한 항목만 골라 추가하세요. 전부 추가할 필요는 없어요.
                      </div>
                    </div>
                    <button
                      class="ai-btn ghost"
                      type="button"
                      @click="dismissSuggestions"
                    >
                      취소
                    </button>
                    <button
                      class="ai-btn primary"
                      type="button"
                      :disabled="selectedCount === 0"
                      @click="commitSelected"
                    >
                      <svg
                        viewBox="0 0 24 24"
                        fill="none"
                        stroke="currentColor"
                        stroke-width="2.2"
                        stroke-linecap="round"
                        stroke-linejoin="round"
                      ><path d="M5 12h14M12 5v14" /></svg>
                      {{ selectedCount === 0 ? '업무에 추가' : selectedCount + '개 업무에 추가' }}
                    </button>
                  </div>
                </div>
              </div>
            </Teleport>
          </div>
        </section>

        <!-- 사이드바 -->
        <aside class="side">
          <div class="card side-card">
            <div class="side-field">
              <div class="side-label">
                <span class="req">*</span> 담당자 <span class="muted">· {{ assignees.length }}명</span>
              </div>
              <div class="combo">
                <!-- 입력칸은 고정 — 선택된 담당자는 아래에 표기 -->
                <div
                  class="combo-field"
                  :class="{ 'has-error': assigneeError }"
                >
                  <input
                    v-model="assigneeQuery"
                    class="combo-input"
                    placeholder="멤버 검색…"
                    autocomplete="off"
                    @focus="showAssigneeDropdown = true"
                    @blur="onAssigneeBlur"
                  >
                  <span class="combo-caret">
                    <svg
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      stroke-width="2"
                      stroke-linecap="round"
                      stroke-linejoin="round"
                    ><path d="m6 9 6 6 6-6" /></svg>
                  </span>
                </div>
                <!-- 멤버 후보 드롭다운 -->
                <div
                  v-if="showAssigneeDropdown"
                  class="assignee-dd"
                >
                  <button
                    v-for="m in assigneeCandidates"
                    :key="m.user.id"
                    type="button"
                    class="assignee-dd-item"
                    @mousedown.prevent="addAssignee(m.user.id)"
                  >
                    <span class="avatar"><UserAvatar
                      :url="m.user.avatarUrl"
                      :name="m.user.name"
                    /></span>
                    <span class="ad-nm">{{ m.user.name }}</span>
                    <span class="ad-em">{{ m.email }}</span>
                  </button>
                  <div
                    v-if="assigneeCandidates.length === 0"
                    class="assignee-dd-msg"
                  >
                    {{ memberStore.members.length === 0 ? '프로젝트 멤버가 없습니다.' : '추가할 멤버가 없습니다.' }}
                  </div>
                </div>
              </div>
              <!-- 선택된 담당자 칩 (입력칸 아래에 표기) -->
              <div
                v-if="assignees.length"
                class="assignee-chips"
              >
                <span
                  v-for="a in assignees"
                  :key="a.id"
                  class="chip"
                >
                  <span class="avatar"><UserAvatar
                    :url="a.avatarUrl"
                    :name="a.name"
                  /></span>
                  {{ a.name }}
                  <span
                    class="x"
                    @click="removeAssignee(a.id)"
                  >×</span>
                </span>
              </div>
              <div
                v-if="assigneeError"
                class="field-error"
              >
                담당자를 1명 이상 지정해 주세요.
              </div>
            </div>
          </div>

          <div class="card side-card">
            <div class="side-field">
              <div class="side-label">
                마감기한 <span class="muted">· 선택</span>
              </div>
              <input
                v-model="dueDate"
                class="input date-input"
                type="date"
              >
            </div>

            <div class="side-field">
              <div class="side-label">
                중간 점검일 <span class="muted">· 선택 · 여러 개</span>
              </div>
              <div class="cp-add">
                <input
                  v-model="newCheckpoint"
                  class="input date-input"
                  type="date"
                >
                <button
                  class="cp-add-btn"
                  type="button"
                  :disabled="!newCheckpoint"
                  @click="addCheckpoint"
                >
                  추가
                </button>
              </div>
              <ul
                v-if="checkpoints.length"
                class="cp-list"
              >
                <li
                  v-for="d in checkpoints"
                  :key="d"
                  class="cp-item"
                >
                  <span>{{ d }}</span>
                  <button
                    class="x"
                    type="button"
                    title="삭제"
                    @click="removeCheckpoint(d)"
                  >
                    ×
                  </button>
                </li>
              </ul>
              <p class="cp-hint">
                담당자가 점검일 당일에 진행 상태를 보고서로 보낼 수 있습니다. (지나면 누락)
              </p>
            </div>

            <div class="side-field">
              <div class="side-label">
                첨부파일 <span
                  v-if="totalFileCount"
                  class="muted"
                >· {{ totalFileCount }}개</span>
              </div>
              <div
                v-if="existingFiles.length || pendingFiles.length"
                class="files"
              >
                <!-- 기존 첨부(수정 모드) -->
                <div
                  v-for="f in existingFiles"
                  :key="f.id"
                  class="file"
                >
                  <span
                    class="file-ico"
                    :class="fileBadgeFor(f.name).cls"
                  >{{ fileBadgeFor(f.name).label }}</span>
                  <span class="file-info">
                    <span class="file-name">{{ f.name }}</span>
                    <span class="file-size">{{ formatBytes(f.size) }}</span>
                  </span>
                  <span
                    class="x"
                    title="삭제"
                    @click="removeExisting(f.id)"
                  >
                    <svg
                      width="15"
                      height="15"
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      stroke-width="2"
                    ><path d="M18 6 6 18M6 6l12 12" /></svg>
                  </span>
                </div>
                <!-- 새로 선택(업로드 대기) -->
                <div
                  v-for="(f, i) in pendingFiles"
                  :key="`p${i}`"
                  class="file"
                >
                  <span
                    class="file-ico"
                    :class="fileBadgeFor(f.name).cls"
                  >{{ fileBadgeFor(f.name).label }}</span>
                  <span class="file-info">
                    <span class="file-name">{{ f.name }}</span>
                    <span class="file-size">{{ formatBytes(f.size) }} · 업로드 대기</span>
                  </span>
                  <span
                    class="x"
                    title="제거"
                    @click="removePending(i)"
                  >
                    <svg
                      width="15"
                      height="15"
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      stroke-width="2"
                    ><path d="M18 6 6 18M6 6l12 12" /></svg>
                  </span>
                </div>
              </div>
              <button
                class="attach-btn"
                type="button"
                @click="triggerFilePicker"
              >
                <svg
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                >
                  <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
                </svg>
                파일 선택
              </button>
              <input
                ref="fileInput"
                type="file"
                multiple
                class="file-input-hidden"
                @change="onPickFiles"
              >
              <div class="file-hint">
                최대 25MB · 이미지·문서·압축 파일. 지시를 보내면 함께 업로드됩니다.
              </div>
            </div>
          </div>

          <div class="card side-card">
            <div class="side-field bare">
              <div class="side-label">
                지시자
              </div>
              <div class="issuer">
                <span class="avatar"><UserAvatar
                  :url="issuer.avatarUrl"
                  :name="issuer.name"
                /></span>
                <span>
                  <div class="nm">{{ issuer.name }}</div>
                </span>
              </div>
              <div class="side-foot">
                {{ isEdit
                  ? '변경 사항을 저장하면 담당자에게 반영됩니다.'
                  : `지시를 보내면 담당자 ${assignees.length}명에게 알림이 전송되고, 업무가 각자의 ‘내 업무’에 추가됩니다.` }}
              </div>
            </div>
          </div>
        </aside>
      </div>
    </div>
  </AppShell>
</template>

<style scoped>
/* 컨텐츠 최대 너비는 전역 .page(67.5rem)를 그대로 사용해 다른 화면과 통일한다. */
.crumb {
  padding: 1rem 0 0;
}

.pagehead {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.5rem 0 1.125rem;
}
.pagehead h1 {
  font-size: 1.3125rem;
  font-weight: 700;
  letter-spacing: -0.025rem;
  white-space: nowrap;
  flex-shrink: 0;
}
.pagehead .actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.pagehead .btn {
  height: 2.125rem;
}
.btn.ai {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  color: var(--accent);
  background: var(--accent-weak);
  border: 1px solid var(--accent-border);
}
.btn.ai:hover:not(:disabled) {
  background: #e7e4fb;
}
.btn.ai:disabled {
  opacity: 0.55;
  cursor: default;
}
.btn.ai svg {
  width: 0.9375rem;
  height: 0.9375rem;
}

/* 레이아웃 */
.layout {
  display: grid;
  grid-template-columns: 1fr 21.75rem;
  gap: 1.25rem;
  align-items: start;
}
.main-card {
  padding: 0.5rem 1.625rem 1.625rem;
}

/* 제목 입력 */
.title-input {
  width: 100%;
  border: none;
  outline: none;
  background: transparent;
  font-family: inherit;
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -0.03125rem;
  color: var(--text);
  padding: 1.25rem 0 0.875rem;
  line-height: 1.3;
}
.title-input::placeholder {
  color: #c4c8cf;
}
.title-input.has-error {
  border: 1px solid var(--red);
  border-radius: var(--radius);
  padding: 1.25rem 0.75rem 0.875rem;
}
.field-error {
  font-size: 0.75rem;
  color: var(--red);
  margin-top: 0.375rem;
}
.divider {
  height: 1px;
  background: var(--border);
  margin: 0 -1.625rem;
}

.field {
  padding-top: 1.375rem;
}
.field-label {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  font-size: 0.781rem;
  font-weight: 600;
  color: var(--text-2);
  margin-bottom: 0.5625rem;
  white-space: nowrap;
}
.field-label .req {
  color: var(--accent);
  font-weight: 700;
}

/* 에디터 */
.editor {
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  overflow: hidden;
}
.editor:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-weak);
}
.editor.has-error {
  border-color: var(--red);
}
.editor.has-error:focus-within {
  box-shadow: 0 0 0 3px var(--red-weak);
}
.editor-toolbar {
  display: flex;
  align-items: center;
  gap: 0.125rem;
  padding: 0.375rem 0.5rem;
  border-bottom: 1px solid var(--border);
  background: #fafbfc;
}
.tb {
  height: 1.75rem;
  min-width: 1.75rem;
  padding: 0 0.4375rem;
  border-radius: var(--radius-sm);
  border: none;
  background: transparent;
  color: var(--text-2);
  font-size: 0.8125rem;
  font-weight: 600;
  cursor: pointer;
  display: grid;
  place-items: center;
  font-family: inherit;
}
.tb:hover {
  background: #eceef1;
  color: var(--text);
}
.tb svg {
  width: 1rem;
  height: 1rem;
}
.tb-sep {
  width: 1px;
  height: 1.125rem;
  background: var(--border-strong);
  margin: 0 0.3125rem;
}
.editor-body {
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: 0.875rem 1rem;
  font-size: 0.875rem;
  font-family: inherit;
  color: var(--text);
  min-height: 9.375rem;
  line-height: 1.65;
  border: none;
  outline: none;
  background: transparent;
  resize: vertical;
}
.editor-body::placeholder {
  color: var(--text-3);
}
.mention {
  color: var(--accent);
  font-weight: 600;
  background: var(--accent-weak);
  padding: 0 0.1875rem;
  border-radius: 3px;
}

/* 체크리스트 */
.check-head {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  margin-bottom: 0.625rem;
}
.progress-wrap {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 0.5625rem;
}
.progress-bar {
  width: 5.75rem;
  height: 0.375rem;
  border-radius: 4px;
  background: #eceef1;
  overflow: hidden;
}
.progress-bar > i {
  display: block;
  height: 100%;
  background: var(--green);
  border-radius: 4px;
}
.progress-txt {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-2);
  white-space: nowrap;
}
/* 카테고리 탭 — 필수/관리/보안/부가 */
.cat-tabs {
  display: flex;
  gap: 0.375rem;
  margin-bottom: 0.5rem;
  flex-wrap: wrap;
}
.cat-tab {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.375rem 0.6875rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: #fff;
  font-size: 0.781rem;
  font-weight: 600;
  color: var(--text-2);
  cursor: pointer;
  transition:
    border-color 0.12s,
    background 0.12s,
    color 0.12s;
}
.cat-tab:hover {
  background: #f7f8fa;
}
.cat-tab .cat-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  flex-shrink: 0;
}
.cat-tab.sec-1 .cat-dot {
  background: var(--accent);
}
.cat-tab.sec-2 .cat-dot {
  background: #1b7a47;
}
.cat-tab.sec-3 .cat-dot {
  background: #c0394f;
}
.cat-tab.sec-4 .cat-dot {
  background: #a96a13;
}
.cat-tab .cat-count {
  font-size: 0.719rem;
  font-weight: 700;
  color: var(--text-3, var(--text-2));
  background: #eceef1;
  border-radius: 999px;
  padding: 0.0625rem 0.375rem;
  min-width: 1.125rem;
  text-align: center;
}
.cat-tab.active {
  color: var(--text);
  border-color: var(--text);
  background: #fff;
}
.cat-tab.active .cat-count {
  background: var(--text);
  color: #fff;
}
.check-empty {
  padding: 0.875rem 0;
  font-size: 0.781rem;
  color: var(--text-2);
}
.checklist {
  display: flex;
  flex-direction: column;
}
.check-item {
  display: flex;
  align-items: center;
  gap: 0.6875rem;
  padding: 0.5625rem 0;
  border-radius: var(--radius-sm);
}
.check-item:hover {
  background: #f7f8fa;
}
.cbox {
  width: 1.125rem;
  height: 1.125rem;
  border-radius: 0.3125rem;
  border: 1.5px solid var(--border-strong);
  flex-shrink: 0;
  display: grid;
  place-items: center;
  background: #fff;
}
.cbox.done {
  background: var(--green);
  border-color: var(--green);
}
.cbox svg {
  width: 0.75rem;
  height: 0.75rem;
  color: #fff;
  opacity: 0;
}
.cbox.done svg {
  opacity: 1;
}
.check-item .txt {
  font-size: 0.844rem;
  color: var(--text);
  flex: 1;
}
.check-item.done .txt {
  color: var(--text-3);
  text-decoration: line-through;
}
.check-item .row-del {
  opacity: 0;
  color: var(--text-3);
  cursor: pointer;
  width: 1.5rem;
  height: 1.5rem;
  display: grid;
  place-items: center;
  border-radius: 4px;
}
.check-item:hover .row-del {
  opacity: 1;
}
.check-item .row-del:hover {
  background: #eceef1;
  color: var(--red);
}
.add-row {
  display: flex;
  align-items: center;
  gap: 0.6875rem;
  padding: 0.625rem 0 0.25rem;
  color: var(--text-3);
  font-size: 0.844rem;
}
.add-row .plus {
  width: 1.125rem;
  height: 1.125rem;
  border-radius: 0.3125rem;
  border: 1.5px dashed var(--border-strong);
  display: grid;
  place-items: center;
  font-size: 0.8125rem;
  line-height: 1;
  flex-shrink: 0;
}
.add-input {
  flex: 1;
  border: none;
  outline: none;
  background: transparent;
  font-family: inherit;
  font-size: 0.844rem;
  color: var(--text);
}
.add-input::placeholder {
  color: var(--text-3);
}

/* AI 추천 할 일 */
.ai-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: rgba(22, 24, 29, 0.46);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.75rem;
}
.ai-modal {
  width: 100%;
  max-width: 37.5rem;
  max-height: calc(100vh - 3.5rem);
  background: #fff;
  border-radius: 1rem;
  box-shadow:
    0 1.5rem 4.375rem rgba(20, 24, 33, 0.3),
    0 0.25rem 0.875rem rgba(20, 24, 33, 0.14);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* 헤더 */
.ai-head {
  position: relative;
  padding: 1.25rem 1.375rem 1.125rem;
  background:
    radial-gradient(120% 140% at 0% 0%, #f0eefe 0%, rgba(240, 238, 254, 0) 55%),
    linear-gradient(180deg, #f6f5ff 0%, #ffffff 100%);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.ai-head-top {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
}
.ai-badge {
  width: 2.375rem;
  height: 2.375rem;
  border-radius: 0.6875rem;
  flex-shrink: 0;
  background: linear-gradient(140deg, #6366f1, #4338ca);
  display: grid;
  place-items: center;
  color: #fff;
  box-shadow:
    0 0.25rem 0.75rem rgba(79, 70, 229, 0.4),
    inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
.ai-badge svg {
  width: 1.3125rem;
  height: 1.3125rem;
}
.ai-htext {
  flex: 1;
  min-width: 0;
  padding-top: 0.0625rem;
}
.ai-title {
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.01875rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.ai-title .auto {
  font-size: 0.656rem;
  font-weight: 700;
  color: var(--accent);
  background: #fff;
  border: 1px solid var(--accent-border);
  border-radius: 20px;
  padding: 0.125rem 0.5rem;
  white-space: nowrap;
}
.ai-sub {
  font-size: 0.781rem;
  color: var(--text-2);
  margin-top: 0.25rem;
  line-height: 1.5;
}
.ai-sub b {
  color: var(--text);
  font-weight: 600;
}
.ai-x {
  position: absolute;
  top: 1rem;
  right: 1rem;
  width: 2rem;
  height: 2rem;
  border-radius: 0.5rem;
  border: none;
  background: transparent;
  color: var(--text-3);
  display: grid;
  place-items: center;
  cursor: pointer;
}
.ai-x:hover {
  background: rgba(20, 24, 33, 0.06);
  color: var(--text);
}
.ai-x:disabled {
  opacity: 0.4;
  cursor: default;
}
.ai-x svg {
  width: 1.125rem;
  height: 1.125rem;
}

/* 본문 */
.ai-body {
  overflow-y: auto;
  padding: 0.375rem 1.375rem 0.5rem;
  flex: 1;
}
.ai-state {
  padding: 2rem 0;
  text-align: center;
  font-size: 0.875rem;
  color: var(--text-2);
}
.ai-state.err {
  color: var(--red);
}
.sec {
  padding: 1rem 0 0.25rem;
}
.sec + .sec {
  border-top: 1px solid var(--border);
}
.sec-head {
  display: flex;
  align-items: center;
  gap: 0.5625rem;
  margin-bottom: 0.625rem;
}
.sec-dot {
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 0.4375rem;
  display: grid;
  place-items: center;
  flex-shrink: 0;
}
.sec-dot svg {
  width: 0.8125rem;
  height: 0.8125rem;
}
.sec-1 .sec-dot {
  background: var(--accent-weak);
  color: var(--accent);
}
.sec-2 .sec-dot {
  background: #e7f3ee;
  color: #1b7a47;
}
.sec-3 .sec-dot {
  background: #fdeef0;
  color: #c0394f;
}
.sec-4 .sec-dot {
  background: #fbf1e0;
  color: #a96a13;
}
.sec-name {
  font-size: 0.844rem;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.0125rem;
  white-space: nowrap;
}
.sec-count {
  font-size: 0.719rem;
  font-weight: 600;
  color: var(--text-2);
  background: #f1f2f4;
  border-radius: 20px;
  padding: 0.0625rem 0.5rem;
}
.sec-all {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.3125rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--accent);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 0.25rem 0.5625rem;
  cursor: pointer;
  font-family: inherit;
  line-height: 1;
  white-space: nowrap;
}
.sec-all:hover {
  background: var(--accent-weak);
}
.sec-all svg {
  width: 0.8125rem;
  height: 0.8125rem;
}
.sec-all.on {
  color: var(--text-2);
}
.sec-all.on:hover {
  background: #f1f2f4;
}

/* 항목 목록 */
.items {
  border: 1px solid var(--border);
  border-radius: 0.625rem;
  overflow: hidden;
  background: #fff;
}
.item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  text-align: left;
  padding: 0.75rem 0.8125rem;
  border: none;
  background: transparent;
  cursor: pointer;
  font-family: inherit;
  border-top: 1px solid var(--border);
  position: relative;
}
.item:first-child {
  border-top: none;
}
.item::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 0.1875rem;
  background: var(--accent);
  opacity: 0;
  transition: opacity 0.12s;
}
.item:hover:not(:disabled) {
  background: #fafaff;
}
.item:disabled {
  cursor: default;
}
.item-check {
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 0.4375rem;
  flex-shrink: 0;
  border: 1.5px solid var(--border-strong);
  background: #fff;
  display: grid;
  place-items: center;
  color: #fff;
  position: relative;
  transition: all 0.13s;
}
.item-check .plus {
  width: 0.8125rem;
  height: 0.8125rem;
  color: var(--text-3);
  transition: opacity 0.12s;
}
.item-check .tick {
  width: 0.8125rem;
  height: 0.8125rem;
  position: absolute;
  opacity: 0;
  transition: opacity 0.12s;
}
.item:hover:not(:disabled) .item-check {
  border-color: var(--accent-border);
}
.item:hover:not(:disabled) .item-check .plus {
  color: var(--accent);
}
.item-text {
  flex: 1;
  font-size: 0.844rem;
  color: var(--text);
  line-height: 1.5;
  letter-spacing: -0.00625rem;
}
.item.added {
  background: #f7f7ff;
}
.item.added::before {
  opacity: 1;
}
.item.added .item-check {
  background: var(--accent);
  border-color: var(--accent);
}
.item.added .item-check .plus {
  opacity: 0;
}
.item.added .item-check .tick {
  opacity: 1;
}
.item.added .item-text {
  color: var(--text-2);
}

/* 푸터 */
.ai-foot {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.8125rem 1.375rem;
  border-top: 1px solid var(--border);
  background: #fbfbfd;
  flex-shrink: 0;
}
.foot-info {
  margin-right: auto;
  min-width: 0;
}
.foot-count {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}
.foot-count .n {
  color: var(--accent);
}
.foot-hint {
  font-size: 0.719rem;
  color: var(--text-3);
  margin-top: 0.125rem;
}
.ai-btn {
  height: 2.375rem;
  padding: 0 1rem;
  border-radius: var(--radius);
  font-size: 0.844rem;
  font-weight: 600;
  border: 1px solid var(--border-strong);
  background: #fff;
  color: var(--text-2);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  line-height: 1;
  font-family: inherit;
  white-space: nowrap;
}
.ai-btn:hover {
  background: #f7f8fa;
  color: var(--text);
}
.ai-btn.ghost {
  border-color: transparent;
  background: transparent;
}
.ai-btn.ghost:hover {
  background: #eceef1;
}
.ai-btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  box-shadow: 0 1px 2px rgba(79, 70, 229, 0.4);
}
.ai-btn.primary:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}
.ai-btn.primary:disabled {
  background: #c3c7ce;
  border-color: #c3c7ce;
  box-shadow: none;
  cursor: not-allowed;
}
.ai-btn svg {
  width: 0.9375rem;
  height: 0.9375rem;
}

/* 사이드바 */
.side {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.side-card {
  padding: 1rem 1.125rem 1.125rem;
}
.side-field + .side-field {
  margin-top: 1.125rem;
  padding-top: 1.125rem;
  border-top: 1px solid var(--border);
}
.side-field.bare {
  border: none;
  padding: 0;
  margin: 0;
}
.side-label {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-2);
  margin-bottom: 0.5625rem;
  display: flex;
  align-items: center;
  gap: 0.375rem;
  white-space: nowrap;
}
.side-label .req {
  color: var(--accent);
}
.side-label .muted {
  color: var(--text-3);
  font-weight: 500;
}

/* 담당자 콤보 */
.combo {
  position: relative;
}
/* 멤버 후보 드롭다운 */
.assignee-dd {
  position: absolute;
  top: calc(100% + 0.375rem);
  left: 0;
  right: 0;
  z-index: 10;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 0.5625rem;
  box-shadow: 0 12px 32px rgba(20, 24, 33, 0.16);
  padding: 0.375rem;
  max-height: 14rem;
  overflow-y: auto;
}
.assignee-dd-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.4375rem 0.5rem;
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-family: inherit;
  text-align: left;
}
.assignee-dd-item:hover {
  background: #f5f6f8;
}
.assignee-dd-item .avatar {
  width: 1.5rem;
  height: 1.5rem;
  font-size: 0.625rem;
  flex-shrink: 0;
}
.ad-nm {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}
.ad-em {
  font-size: 0.719rem;
  color: var(--text-3);
  margin-left: auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 9rem;
}
.assignee-dd-msg {
  padding: 0.625rem 0.5rem;
  font-size: 0.75rem;
  color: var(--text-3);
  text-align: center;
}
.date-input {
  cursor: pointer;
}
.combo-field {
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0.4375rem 0.5rem;
  background: #fff;
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  align-items: center;
}
.combo-field:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-weak);
}
.combo-field.has-error {
  border-color: var(--red);
}
.combo-field.has-error:focus-within {
  box-shadow: 0 0 0 3px var(--red-weak);
}
.combo-caret {
  color: var(--text-3);
  display: grid;
  place-items: center;
  margin-left: auto;
  flex-shrink: 0;
}
.combo-caret svg {
  width: 1rem;
  height: 1rem;
}
/* 선택된 담당자 칩 — 입력칸 아래에 표기 */
.assignee-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin-top: 0.5rem;
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  background: #f1f2f4;
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 0.1875rem 0.5rem 0.1875rem 0.1875rem;
  font-size: 0.781rem;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}
.chip .avatar {
  width: 1.375rem;
  height: 1.375rem;
  font-size: 0.656rem;
}
.chip .x {
  color: var(--text-3);
  cursor: pointer;
  font-size: 0.8125rem;
  line-height: 1;
  margin-left: 0.0625rem;
}
.chip .x:hover {
  color: var(--red);
}
.combo-input {
  flex: 1;
  min-width: 4.375rem;
  border: none;
  outline: none;
  font-family: inherit;
  font-size: 0.8125rem;
  padding: 0.25rem 0.125rem;
  background: transparent;
}
.combo-input::placeholder {
  color: var(--text-3);
}

/* 마감기한 입력 */
.input {
  width: 100%;
  height: 2.375rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0 0.6875rem;
  font-family: inherit;
  font-size: 0.844rem;
  color: var(--text);
  background: #fff;
  display: flex;
  align-items: center;
  gap: 0.5625rem;
}
.input .ico {
  color: var(--text-3);
  display: grid;
  place-items: center;
}
/* 중간 점검일 입력/목록 */
.cp-add {
  display: flex;
  gap: 0.5rem;
}
.cp-add .date-input {
  flex: 1;
}
.cp-add-btn {
  flex-shrink: 0;
  border: 1px solid var(--border-strong);
  background: #fff;
  font-family: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-2);
  padding: 0 0.75rem;
  border-radius: var(--radius);
  cursor: pointer;
}
.cp-add-btn:hover:not(:disabled) {
  background: #f5f6f8;
}
.cp-add-btn:disabled {
  opacity: 0.5;
  cursor: default;
}
.cp-list {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.cp-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 0.8125rem;
  color: var(--text);
  background: #f5f6f8;
  border-radius: var(--radius-sm);
  padding: 0.3125rem 0.5rem 0.3125rem 0.625rem;
}
.cp-item .x {
  cursor: pointer;
  color: var(--text-3);
  border: none;
  background: transparent;
  font-size: 1rem;
  line-height: 1;
  padding: 0 0.25rem;
}
.cp-item .x:hover {
  color: var(--red);
}
.cp-hint {
  font-size: 0.75rem;
  color: var(--text-3);
  margin-top: 0.4375rem;
  line-height: 1.5;
}
.input .ico svg {
  width: 1rem;
  height: 1rem;
}
.date-meta {
  font-size: 0.75rem;
  color: var(--amber);
  font-weight: 600;
  margin-top: 0.4375rem;
  display: flex;
  align-items: center;
  gap: 0.3125rem;
}
.date-meta .dot {
  width: 0.375rem;
  height: 0.375rem;
  border-radius: 50%;
  background: var(--amber);
}

/* 첨부파일 */
.attach-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  height: 2.25rem;
  padding: 0 0.875rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: #fff;
  color: var(--text-2);
  font-family: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  cursor: pointer;
}
.attach-btn:hover {
  background: #f7f8fa;
  color: var(--text);
}
.attach-btn svg {
  width: 0.875rem;
  height: 0.875rem;
}
.files {
  display: flex;
  flex-direction: column;
  gap: 0.4375rem;
  margin-bottom: 0.625rem;
}
.file {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.5rem 0.5625rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #fff;
}
.file-ico {
  width: 1.875rem;
  height: 1.875rem;
  border-radius: 0.375rem;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  font-size: 0.625rem;
  font-weight: 800;
  color: #fff;
  letter-spacing: -0.01875rem;
}
.fi-pdf {
  background: #e25950;
}
.fi-zip {
  background: #8a64d6;
}
.fi-doc {
  background: #3d8bf2;
}
.fi-img {
  background: #2aa775;
}
.file-info {
  min-width: 0;
  flex: 1;
}
.file-name {
  font-size: 0.781rem;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: block;
}
.file-size {
  font-size: 0.719rem;
  color: var(--text-3);
  display: block;
}
.file .x {
  color: var(--text-3);
  cursor: pointer;
  width: 1.375rem;
  height: 1.375rem;
  display: grid;
  place-items: center;
  border-radius: 4px;
  flex-shrink: 0;
}
.file .x:hover {
  background: #eceef1;
  color: var(--red);
}
.file-input-hidden {
  display: none;
}
.file-hint {
  margin-top: 0.4375rem;
  font-size: 0.719rem;
  color: var(--text-3);
  line-height: 1.45;
}

/* 지시자 */
.issuer {
  display: flex;
  align-items: center;
  gap: 0.625rem;
}
.issuer .avatar {
  width: 1.875rem;
  height: 1.875rem;
  font-size: 0.75rem;
}
.issuer .nm {
  font-size: 0.8125rem;
  font-weight: 600;
}
.side-foot {
  font-size: 0.719rem;
  color: var(--text-3);
  line-height: 1.6;
  margin-top: 0.75rem;
}
</style>
