generated from ttipo/project-base
chore: 문구 다듬기 브랜치(14단계 · main 을 합친 판)를 앱 서버 브랜치에 합친다
CI (Flutter) / Flutter (analyze · test) (pull_request) Successful in 23m47s
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 14m26s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 1m42s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m20s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 13m4s
CI (Flutter) / Flutter (analyze · test) (push) Has been cancelled
CI / 에러 코드 SSOT 동기화 검사 (push) Has been cancelled
CI / Backend (lint · type-check · test · e2e · build) (push) Has been cancelled
CI / Frontend (lint · type-check · test · build) (push) Has been cancelled
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Has been cancelled
CI (Flutter) / Flutter (analyze · test) (pull_request) Successful in 23m47s
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 14m26s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 1m42s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m20s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 13m4s
CI (Flutter) / Flutter (analyze · test) (push) Has been cancelled
CI / 에러 코드 SSOT 동기화 검사 (push) Has been cancelled
CI / Backend (lint · type-check · test · e2e · build) (push) Has been cancelled
CI / Frontend (lint · type-check · test · build) (push) Has been cancelled
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Has been cancelled
이 PR 이 문구 다듬기 PR 뒤에 빨리 감기로 합쳐지고 CI 가 합친 모습 그대로를 시험하게 한다.
충돌 넷은 모두 설정 › 로그인된 기기의 글이다 — 앱 쪽은 앱 세션이 함께 보여 "브라우저"를 "기기"로,
다듬기 쪽은 같은 안내 문장을 규칙 9 로("여기에 보이지 않습니다" → "이 목록에 없습니다") 바꿨다.
뜻은 앱 쪽 · 문장은 다듬은 글로 풀었고(account-security.ts · LoginSessionList.vue 주석 · 디자인 README),
시험은 두 쪽이 더한 상수를 모두 남기고 글자 그대로 견주던 줄을 상수로 바꿨다(AccountLoginHistoryPage.spec.ts).
합친 뒤 backend tsc · lint 0 · 단위 3,701 · e2e 333, frontend type-check · lint 0 · 단위 2,774 통과.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #20.
This commit is contained in:
@@ -18,7 +18,7 @@ interface Props {
|
||||
tone?: 'neutral' | 'danger'
|
||||
/** 서버 추적 ID — 앞 8자리를 "문의 코드"로 보인다 */
|
||||
traceId?: string | null
|
||||
/** 영역을 세로로 가득 채워 가운데 둔다 (본문 영역의 "메일을 선택해 주세요") */
|
||||
/** 영역을 세로로 가득 채워 가운데 둔다 (본문 영역의 "고른 메일이 없습니다") */
|
||||
fill?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// "같은 대화" 칸에서 펼친 줄의 안 (14단계 — 디자인 원본 Thread14 "하나 펼침")
|
||||
// 받는 사람 · 제목 한 줄씩, 본문(MailBodyFrame — 검색 쿼리 없이 받은 본문이라 강조가 없다), [답장] · [이 메일만 열기](36 — 밀도 예외 --size-notice-action 재사용).
|
||||
// - 본문이 다 보이지 않으면(서버의 isHtmlBodyOmitted · isBodyTruncated) 읽기 화면과 같은 안내 줄(MAIL_BODY_NOTICES)을 본문 위에 — 읽기 화면도 같은 본문이다.
|
||||
// 앞 문장만 보이고 원문 내려받기(할 일)는 붙이지 않는다 — 이 화면의 ⋯ 는 위에 연 메일의 것이라, 원문은 [이 메일만 열기]로 연 읽기 화면에서 받는다.
|
||||
// 외부 이미지 안내 · 첨부 목록은 두지 않는다 — [이 메일만 열기]로 연 읽기 화면에 있다. 첨부가 있으면 본문 아래 · [답장] 위에
|
||||
// "첨부 파일 N개는 [이 메일만 열기]를 누르면 볼 수 있습니다." (수는 읽기 화면의 "첨부 파일 N개"와 같다 — 본문 그림 빼고 · 대용량 첨부 더해)
|
||||
// - 본문을 받는 동안은 뼈대(aria-busy), 못 받으면 사전 문구 + [다시 시도]. 그사이 지워진 메일(RES_001)이면 [다시 시도] · [답장] · [이 메일만 열기] 없이 —
|
||||
@@ -49,7 +50,7 @@ const ready = computed(() => (props.detail?.status === 'ready' ? props.detail.de
|
||||
const failure = computed(() => (props.detail?.status === 'error' ? props.detail.error : null))
|
||||
/** 그사이 지워진 메일 — 다시 해도 같고 그 메일로 갈 곳도 없다 */
|
||||
const missing = computed(() => failure.value?.code === 'RES_001')
|
||||
const bodyNotices = computed(() => (ready.value ? mailBodyNotices(ready.value) : []))
|
||||
const bodyNotices = computed(() => (ready.value ? mailBodyNotices(ready.value, { withAction: false }) : []))
|
||||
/** 읽기 화면에서 볼 수 있는 첨부 수 — 본문 그림(isInline)은 빼고 대용량 첨부는 더한다 (MessageReader 의 첨부 칸과 같은 수) */
|
||||
const attachmentCount = computed(() =>
|
||||
ready.value ? listedAttachments(ready.value.attachments).length + (ready.value.largeFiles?.length ?? 0) : 0,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia, type Pinia } from 'pinia'
|
||||
|
||||
import { MAILBOX_EMPTY_HINTS } from '@/shared/constants/mail'
|
||||
import type { Mailbox, MessageSummary } from '@/shared/types/mail'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
import { useMailStore } from '@/stores/mail.store'
|
||||
@@ -239,7 +240,9 @@ describe('MailListFrame — 휴지통 · 스팸메일함의 30일 줄 (13단계)
|
||||
|
||||
expect(wrapper.find('[data-retention-notice]').exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('휴지통이 비어 있습니다')
|
||||
expect(wrapper.text()).toContain('삭제한 메일이 여기에 표시됩니다. 들어간 지 30일이 지난 메일은 자동으로 지워집니다.')
|
||||
expect(wrapper.text()).toContain(MAILBOX_EMPTY_HINTS.trash)
|
||||
// 줄 대신 이 글이 30일을 알린다
|
||||
expect(MAILBOX_EMPTY_HINTS.trash).toContain('30일')
|
||||
})
|
||||
|
||||
it('다른 메일함 · 검색 결과에는 없다', async () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createPinia, setActivePinia, type Pinia } from 'pinia'
|
||||
|
||||
import { CLOCK } from '@/composables/useClock'
|
||||
import { ERROR_CODE_MESSAGES } from '@/shared/constants/error-codes.generated'
|
||||
import { SEARCH_CONDITIONS_TEXT } from '@/shared/constants/mail-search'
|
||||
import type { Mailbox } from '@/shared/types/mail'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
import { useMailStore } from '@/stores/mail.store'
|
||||
@@ -245,9 +246,7 @@ describe('MailSearchBox — [조건] · 「조건으로 찾기」 (14단계 디
|
||||
'최근 1년',
|
||||
'날짜 고르기',
|
||||
])
|
||||
expect(panel.text()).toContain(
|
||||
'검색 칸의 말과 이 조건에 모두 맞는 메일을 찾습니다. 받는 사람에는 참조로 받은 사람도 들어갑니다.',
|
||||
)
|
||||
expect(panel.text()).toContain(SEARCH_CONDITIONS_TEXT.hint)
|
||||
// 칸 안내 — 내용 칸은 칸 이름과 같은 말로 (2026-11-06 실사용 시험)
|
||||
expect(['from', 'to', 'subject', 'body'].map((name) => field(wrapper, name).attributes('placeholder'))).toEqual([
|
||||
'이름이나 메일 주소',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
// 검색 결과 없음 (디자인 원본 Search · Search14 "결과 없음") — 더 찾을 것이 없을 때(hasNext false)만 보인다(이어 찾을 것이 남았으면 MailSearchMore)
|
||||
// 검색 아이콘 · "‘계약’에 맞는 메일이 없습니다"(조건만으로 찾았으면 "조건에 맞는 메일이 없습니다") · 도움말 —
|
||||
// 검색 아이콘 · "‘계약’이 들어간 메일이 없습니다"(조건만으로 찾았으면 "조건에 맞는 메일이 없습니다") · 도움말 —
|
||||
// 검색어와 조건이 함께면 "다른 말로 찾거나 조건을 줄여 보세요.", 조건만이면 "조건을 바꾸거나 줄여 보세요.", 걸린 조건이 없으면 "다른 말로 찾아 보세요."
|
||||
// 지난 메일을 검색할 수 있게 준비하는 동안(엔진 방식 — 낱말 단위)은 그 한계를 한 줄 더한다 — 준비가 끝나면 뺀다 (14단계)
|
||||
// 한 메일함에서 찾았으면 [전체 메일함에서 찾기]
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createPinia, setActivePinia, type Pinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { ERROR_CODE_MESSAGES } from '@/shared/constants/error-codes.generated'
|
||||
import { MAILBOX_OPERATION_TEXT } from '@/shared/constants/mailbox-manage'
|
||||
import type { MailVacationState, Mailbox } from '@/shared/types/mail'
|
||||
import { endOfDayIso } from '@/shared/utils/mail-vacation'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
@@ -433,7 +434,7 @@ describe('MailboxPanel — 메일함 관리', () => {
|
||||
await choose(wrapper, '받은편지함', '모두 읽음으로 표시')
|
||||
// 진행 알림은 그 자리에서 결과로 바뀐다 — 까닭(사전 문구 · 문의 코드)은 인터셉터의 알림
|
||||
expect(useToastStore().toasts.map((toast) => [toast.kind, toast.message])).toEqual([
|
||||
['error', '1,234통 가운데 500통까지 처리하고 멈췄습니다. 다시 시도해 주세요.'],
|
||||
['error', MAILBOX_OPERATION_TEXT.failed('500', '1,234')],
|
||||
['error', ERROR_CODE_MESSAGES.MAIL_001],
|
||||
])
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createPinia, setActivePinia, type Pinia } from 'pinia'
|
||||
|
||||
import { CLOCK, type Clock } from '@/composables/useClock'
|
||||
import { ERROR_CODE_MESSAGES } from '@/shared/constants/error-codes.generated'
|
||||
import { MAIL_BODY_NOTICES } from '@/shared/constants/mail'
|
||||
import { MAIL_BODY_NOTICE_ACTIONS, MAIL_BODY_NOTICES } from '@/shared/constants/mail'
|
||||
import { MAIL_CONVERSATION_PAGE_SIZE, MAIL_CONVERSATION_REFRESH_GAP_MS } from '@/shared/constants/mail-conversation'
|
||||
import type { Mailbox, MessageDetail, MessageSummary } from '@/shared/types/mail'
|
||||
import type { MailListSource } from '@/shared/utils/mail-list-source'
|
||||
@@ -444,7 +444,7 @@ describe('MessageConversation — 읽기 화면의 같은 대화', () => {
|
||||
await rowOf(wrapper, 'm2').get('button').trigger('click')
|
||||
await flushPromises()
|
||||
const error = rowOf(wrapper, 'm2').get('[data-conversation-detail-error]')
|
||||
expect(error.text()).toContain('메일 서비스에 연결할 수 없습니다.')
|
||||
expect(error.text()).toContain(ERROR_CODE_MESSAGES.MAIL_001)
|
||||
expect(useToastStore().toasts).toEqual([])
|
||||
// 읽지 못했으니 읽음으로 바꾸지 않는다
|
||||
expect(requests.some((request) => request.method === 'PATCH')).toBe(false)
|
||||
@@ -857,7 +857,7 @@ describe('MessageConversation — 읽기 화면의 같은 대화', () => {
|
||||
})
|
||||
|
||||
describe('펼친 줄의 본문 안내 · 그사이 지워진 메일', () => {
|
||||
it('서식을 뺐거나 잘린 본문이면 읽기 화면과 같은 안내 줄을 본문 위에 둔다', async () => {
|
||||
it('서식을 뺐거나 잘린 본문이면 읽기 화면과 같은 안내 줄을 본문 위에 둔다 — 앞 문장만(원문은 [이 메일만 열기]로 연 읽기 화면에서)', async () => {
|
||||
serve({ detail: () => ok(detail(REPLY, { isHtmlBodyOmitted: true, isBodyTruncated: true, textBody: '글자 본문' })) })
|
||||
const { wrapper } = await mountReader()
|
||||
|
||||
@@ -869,6 +869,8 @@ describe('MessageConversation — 읽기 화면의 같은 대화', () => {
|
||||
['htmlOmitted', MAIL_BODY_NOTICES.htmlOmitted],
|
||||
['truncated', MAIL_BODY_NOTICES.truncated],
|
||||
])
|
||||
// 이 화면의 ⋯ 는 위에 연 메일의 것이라 할 일(⋯ → 원문 내려받기)은 붙이지 않는다
|
||||
expect(row.text()).not.toContain(MAIL_BODY_NOTICE_ACTIONS.truncated)
|
||||
expect(row.find('iframe').exists()).toBe(true)
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import MailContactDialogHost from '@/components/mail/MailContactDialogHost.vue'
|
||||
import { createMailContactDialog, MAIL_CONTACT_DIALOG } from '@/composables/useMailContactDialog'
|
||||
|
||||
import { ERROR_CODE_MESSAGES } from '@/shared/constants/error-codes.generated'
|
||||
import { MAIL_BODY_NOTICES } from '@/shared/constants/mail'
|
||||
import { MAIL_BODY_NOTICE_ACTIONS, MAIL_BODY_NOTICES } from '@/shared/constants/mail'
|
||||
import type { Contact } from '@/shared/types/contacts'
|
||||
import type { Mailbox, MailFilterRule, MailFilterSet, MessageDetail, MessagesPatch, MessageSummary } from '@/shared/types/mail'
|
||||
import type { PaginatedResult } from '@/shared/types/pagination'
|
||||
@@ -94,12 +94,12 @@ describe('MessageReader 도구 모음', () => {
|
||||
expect(linkHref(wrapper, '전달')).toBe('/mail/compose?forward=m1')
|
||||
})
|
||||
|
||||
it('목록 보기에서는 메일을 못 불러와도 앞쪽 이동 도구(목록으로 등)가 남고, 메일 동작 버튼은 숨긴다', async () => {
|
||||
it('목록 보기에서는 메일을 못 불러와도 앞쪽 이동 도구(목록으로 등)가 남고, 이 메일로 할 일 단추는 숨긴다', async () => {
|
||||
const store = useMailStore()
|
||||
store.selected = null
|
||||
store.selectedId = 'm1'
|
||||
store.selectedStatus = 'error'
|
||||
store.selectedError = { code: 'RES_001', message: '요청하신 정보나 페이지를 찾을 수 없습니다.' }
|
||||
store.selectedError = { code: 'RES_001', message: ERROR_CODE_MESSAGES.RES_001 }
|
||||
const router = createTestRouter()
|
||||
await router.push('/mail/inbox/m1')
|
||||
await router.isReady()
|
||||
@@ -110,7 +110,7 @@ describe('MessageReader 도구 모음', () => {
|
||||
})
|
||||
|
||||
expect(wrapper.find('.test-nav').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('요청하신 정보나 페이지를 찾을 수 없습니다.')
|
||||
expect(wrapper.text()).toContain(ERROR_CODE_MESSAGES.RES_001)
|
||||
expect(wrapper.findAll('.tool-button')).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -159,7 +159,10 @@ describe('MessageReader 본문 안내 (계약 2절 "읽기 한도")', () => {
|
||||
expect(notices(withText)).toEqual([{ kind: 'htmlOmitted', text: MAIL_BODY_NOTICES.htmlOmitted }])
|
||||
|
||||
const withoutText = await mountWith({ ...DETAIL, isHtmlBodyOmitted: true, textBody: null })
|
||||
expect(notices(withoutText)).toEqual([{ kind: 'unavailable', text: MAIL_BODY_NOTICES.unavailable }])
|
||||
// 읽기 화면은 앞 문장 뒤에 할 일(⋯ → 원문 내려받기)까지
|
||||
expect(notices(withoutText)).toEqual([
|
||||
{ kind: 'unavailable', text: `${MAIL_BODY_NOTICES.unavailable} ${MAIL_BODY_NOTICE_ACTIONS.unavailable}` },
|
||||
])
|
||||
})
|
||||
|
||||
it('생략 → 잘림 순서로, 외부 이미지 차단 안내 앞에 같은 모양으로 둔다', async () => {
|
||||
@@ -172,7 +175,7 @@ describe('MessageReader 본문 안내 (계약 2절 "읽기 한도")', () => {
|
||||
})
|
||||
|
||||
expect(notices(wrapper).map((notice) => notice.kind)).toEqual(['truncated', 'blockedImages'])
|
||||
expect(notices(wrapper)[0]?.text).toBe(MAIL_BODY_NOTICES.truncated)
|
||||
expect(notices(wrapper)[0]?.text).toBe(`${MAIL_BODY_NOTICES.truncated} ${MAIL_BODY_NOTICE_ACTIONS.truncated}`)
|
||||
|
||||
const both = await mountWith({ ...DETAIL, isHtmlBodyOmitted: true, isBodyTruncated: true, textBody: '긴 글' })
|
||||
expect(notices(both).map((notice) => notice.kind)).toEqual(['htmlOmitted', 'truncated'])
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia, type Pinia } from 'pinia'
|
||||
|
||||
import { MAIL_IMAGES_TEXT } from '@/shared/constants/mail'
|
||||
import type { BlockedSender, Mailbox, MessageDetail, MessagesPatch, MessageSummary } from '@/shared/types/mail'
|
||||
import { formatScheduleTime } from '@/shared/utils/format'
|
||||
import type { MailListSource } from '@/shared/utils/mail-list-source'
|
||||
@@ -179,7 +180,7 @@ describe('MessageReader — 9단계', () => {
|
||||
extra: (request) => (request.url === '/messages/m1' ? ok(SHOWN) : undefined),
|
||||
})
|
||||
const notice = wrapper.get('[data-notice="blockedImages"]')
|
||||
expect(notice.text()).toContain('외부 이미지를 차단했습니다.')
|
||||
expect(notice.text()).toContain(MAIL_IMAGES_TEXT.blocked)
|
||||
const button = notice.get('[data-show-images]')
|
||||
expect(button.text()).toBe('이미지 보기')
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ onBeforeUnmount(() => {
|
||||
<div
|
||||
class="reader__toolbar"
|
||||
role="group"
|
||||
aria-label="메일 동작"
|
||||
aria-label="이 메일로 할 일"
|
||||
>
|
||||
<slot name="toolbar-start" />
|
||||
<template v-if="message">
|
||||
@@ -301,11 +301,11 @@ onBeforeUnmount(() => {
|
||||
<StateMessage
|
||||
v-else
|
||||
icon="mail"
|
||||
title="메일을 선택해 주세요"
|
||||
title="고른 메일이 없습니다"
|
||||
:hint="
|
||||
mailStore.search
|
||||
? '검색 결과에서 메일을 고르면 내용이 여기에 표시됩니다.'
|
||||
: '목록에서 메일을 고르면 내용이 여기에 표시됩니다.'
|
||||
? '읽을 메일을 검색 결과에서 골라 주세요.'
|
||||
: '읽을 메일을 목록에서 골라 주세요.'
|
||||
"
|
||||
fill
|
||||
/>
|
||||
|
||||
@@ -54,7 +54,8 @@ const senderBlockEntry = computed(() => {
|
||||
if (!junkId || !props.message.mailboxIds.includes(junkId)) return null
|
||||
return blockStore.blockOf(props.message.from?.email)
|
||||
})
|
||||
const bodyNotices = computed(() => mailBodyNotices(props.message))
|
||||
/** 읽기 화면은 할 일까지 — "… [⋯] → [원문 내려받기 (.eml)]로 받아 볼 수 있습니다." (이 화면의 ⋯ 는 이 메일의 것이다) */
|
||||
const bodyNotices = computed(() => mailBodyNotices(props.message, { withAction: true }))
|
||||
const imagesLoading = computed(() => mailStore.imagesLoadingId === props.message.id)
|
||||
|
||||
async function showImages() {
|
||||
|
||||
@@ -286,7 +286,7 @@ describe('RecipientField', () => {
|
||||
expect(options(wrapper)).toEqual(['이도윤 doyun.lee@arasystem.it.kr'])
|
||||
expect(activeOption(wrapper)).toBeNull()
|
||||
expect(input.attributes('aria-activedescendant')).toBeUndefined()
|
||||
expect(wrapper.get('#compose-to-suggestions-hint').text()).toBe('Enter 는 친 주소를 그대로 넣습니다')
|
||||
expect(wrapper.get('#compose-to-suggestions-hint').text()).toBe('Enter를 누르면 친 주소를 그대로 넣습니다')
|
||||
|
||||
await input.trigger('keydown', { key: 'Enter' })
|
||||
expect(tokensOf(wrapper).map((token) => token.label)).toEqual(['doyun.lee@partner.example'])
|
||||
|
||||
@@ -72,7 +72,7 @@ const emit = defineEmits<Emits>()
|
||||
const suggestions = inject(RECIPIENT_SUGGESTIONS, null)
|
||||
|
||||
const HINT_DEFAULT = '↑↓ 고르기 · Enter · Tab 넣기 · Esc 닫기'
|
||||
const HINT_TYPED_ADDRESS = 'Enter 는 친 주소를 그대로 넣습니다'
|
||||
const HINT_TYPED_ADDRESS = 'Enter를 누르면 친 주소를 그대로 넣습니다'
|
||||
|
||||
/** 아직 토큰이 되지 않은 입력 — v-model (한글 조합이 끝나야 바뀐다) */
|
||||
const draft = ref<string>('')
|
||||
@@ -354,7 +354,7 @@ defineExpose({ commit, focus, clearDraft, hasDraft })
|
||||
<span
|
||||
v-if="!token.valid"
|
||||
class="visually-hidden"
|
||||
>(주소 형식 오류)</span>
|
||||
>(주소가 올바르지 않음)</span>
|
||||
<button
|
||||
type="button"
|
||||
class="recipient-chip__remove"
|
||||
|
||||
@@ -117,7 +117,7 @@ function onAddressInput() {
|
||||
v-model="text"
|
||||
type="text"
|
||||
class="link-form__input"
|
||||
placeholder="비우면 주소를 그대로 보여 줍니다"
|
||||
placeholder="비워 두면 주소를 그대로 씁니다"
|
||||
autocomplete="off"
|
||||
@keydown.enter="onEnter"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// 앱 비밀번호를 만든 결과 창 — 모달 620(wide) · 처음 초점 [복사] (디자인 원본 SettingsAppPasswords "만든 결과 창" · 계약 5절 "앱 비밀번호")
|
||||
// "앱 비밀번호를 만들었습니다" / "‘회사 PC 아웃룩’ — 메일 프로그램의 비밀번호 칸에 아래 값을 넣어 주세요."
|
||||
// - 값은 고정폭으로 4자씩 떨어뜨려 보이지만 띄어쓰기는 없다(조각 사이를 여백으로 — 골라 복사해도 값 그대로) — [복사]는 값 그대로 복사한다.
|
||||
// "40자입니다. 보기 좋게 4자씩 띄웠지만 띄어쓰기는 없습니다." — 글자 수는 받은 값의 길이로 쓴다.
|
||||
// "40자입니다. 보기 편하게 4자씩 띄어 놓았지만, 실제로는 띄어쓰기 없이 넣으면 됩니다." — 글자 수는 받은 값의 길이로 쓴다.
|
||||
// 묶음은 줄바꿈 없는 inline-block 덩어리 — 넓으면 한 줄, 좁으면 묶음 사이에서만 줄이 바뀐다(묶음이 둘로 갈리지 않는다 — 2026-10-31 브라우저 점검)
|
||||
// - 경고 "이 창을 닫으면 다시 볼 수 없습니다. …" + 설정값 요약 + [닫기]
|
||||
// - 복사 「앱 비밀번호를 복사했습니다.」 / 못 하면 「앱 비밀번호를 복사하지 못했습니다. 값을 직접 골라 복사해 주세요.」
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// 「로그인된 기기」 + "웹메일 3곳"(앱 세션이 있으면 "웹메일 2곳 · ARA SYSTEM 앱 1곳") · 오른쪽 [다른 곳 모두 로그아웃](44 — 다른 곳이 없으면 그리지 않는다)
|
||||
// 표(구현은 실제 table): PC · 휴대폰 아이콘(16 — 앱 세션은 늘 휴대폰) · 기기 "Edge · Windows" · "ARA SYSTEM 앱 · Android" / IP / 마지막 활동 / [로그아웃](36 — 밀도 예외).
|
||||
// 맨 위는 지금 이 브라우저 — 단추 대신 「지금 이 브라우저」 글자만. 모르는 브라우저는 "알 수 없는 브라우저", 모르는 IP 는 「—」.
|
||||
// 아래 "메일 프로그램은 여기에 보이지 않습니다. 연결을 끊으려면 앱 비밀번호를 지워 주세요."(링크 — 다른 곳이 없으면 앞에 "다른 곳에 로그인된 기기가 없습니다.")
|
||||
// 아래 "메일 프로그램은 이 목록에 없습니다. 연결을 끊으려면 앱 비밀번호를 지워 주세요."(앱 비밀번호는 링크 — 다른 곳이 없으면 앞에 "다른 곳에 로그인된 기기가 없습니다.")
|
||||
// - 줄의 [로그아웃]은 묻지 않고 바로 끊는다 → 「‘Edge · Windows’에서 로그아웃했습니다.」(되돌리기 없음). 누른 줄이 사라지면 초점은 다음 줄(없으면 앞 줄 · 묶음 제목)
|
||||
// - [다른 곳 모두 로그아웃] → 확인 창(처음 초점 「취소」) → 「다른 2곳에서 로그아웃했습니다.」 — 지금 이 브라우저만 남고 초점은 묶음 제목으로
|
||||
// 지금 이 브라우저 줄이 없으면(서버가 이 세션을 알아보지 못함 — 서버는 모두 끊는다) 확인 창이 이 브라우저도 로그아웃된다고 알리고,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// 고치기 창의 「받은편지함에 이미 있는 메일에도 적용」 (7단계 — 디자인 원본 MailFilterApply · MailFilterApplyStates) — 할 일 아래, 6단계의 안내 띠 자리
|
||||
// 고치기 창의 「받은편지함에 이미 있는 메일도 정리하기」 (7단계 — 디자인 원본 MailFilterApply · MailFilterApplyStates) — 할 일 아래, 6단계의 안내 띠 자리
|
||||
// 회색 상자(바탕 #F8F9FB) 하나: 체크 칸(이름 굵게) + 칸 아래 설명(12.5 — 칸의 aria-describedby) → 상자 아래 한 줄 "스팸으로 판정되어 …"
|
||||
// - 켤지는 부모가 정한다(창을 열 때마다 꺼짐 — 필터의 설정이 아니라 이번 저장에 한 번 쓴다)
|
||||
// - 고를 수 없으면(block — mail-filter-run.ts 의 filterApplyBlock) 칸을 disabled · 이름을 흐리게, 설명이 까닭을 말한다:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// 필터 고치기 창 (디자인 원본 SettingsFilter "새 필터" · "필터 고치기" — 모달 620, 공용 모양 ModalDialog 의 넓은 창)
|
||||
// 이름(선택, 50자) → "어떤 메일에"(MailFilterConditionFields — 모두 · 하나라도 · 모든 메일 + 조건 줄 5개까지) →
|
||||
// 할 일(MailFilterActionFields) → 「받은편지함에 이미 있는 메일에도 적용」(MailFilterApplyField — 7단계, 6단계 안내 띠 자리) →
|
||||
// 할 일(MailFilterActionFields) → 「받은편지함에 이미 있는 메일도 정리하기」(MailFilterApplyField — 7단계, 6단계 안내 띠 자리) →
|
||||
// [이 필터 지우기](고칠 때만, 왼쪽) · [저장] [취소].
|
||||
// - 잘못은 [저장]을 누를 때 칸 아래 빨간 글로만 — 빈 조건 · `${` · 할 일 없음 · 전달 주소(빈 칸 · 형식 · 사내 주소가 아님 · 내 주소) ·
|
||||
// 이름 · 찾을 글의 길이 · 제어 문자(사전의 VAL_001).
|
||||
@@ -11,7 +11,7 @@
|
||||
// - 열 때마다 칸을 새로 채운다 — 새 필터는 빈 조건 한 줄(읽기 화면에서 왔으면 "보낸 사람 · 포함 · 그 주소"), 고치기는 그 규칙.
|
||||
// 처음 초점은 이름 칸, 읽기 화면에서 만든 새 필터는 할 일의 첫 칸 (디자인 원본 MailFilterStates)
|
||||
// - 보내는 · 다시 받는 동안(pending)과 다른 저장을 기다리는 동안(busy)은 [저장] · [이 필터 지우기]를 aria-disabled 로 막는다 — 누른 단추가 초점을 잃지 않게.
|
||||
// - 「받은편지함에 이미 있는 메일에도 적용」은 열 때마다 꺼져 있다(필터에 저장하지 않는다). 이 탭에서 다른 적용이 진행 중(applyBusy)이거나
|
||||
// - 「받은편지함에 이미 있는 메일도 정리하기」는 열 때마다 꺼져 있다(필터에 저장하지 않는다). 이 탭에서 다른 적용이 진행 중(applyBusy)이거나
|
||||
// 이미 받은 메일에 할 일이 없으면(옮길 메일함이 지워짐 · 받은편지함으로 옮기기만 · 전달만 — filterApplyBlock) 고를 수 없다.
|
||||
// 고른 채 [저장]하면 save 의 apply 가 true — 저장한 뒤의 적용 · 알림은 부모가 한다(useMailFilterActions)
|
||||
// - 나가기 확인(7단계 — 6단계 한계를 고친다, useDialogLeaveConfirm): 창을 연 때의 값(새 필터는 빈 값 · 읽기 화면에서 채운 값)과 다르면
|
||||
@@ -84,7 +84,7 @@ interface Props {
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
/** apply — 「받은편지함에 이미 있는 메일에도 적용」을 골랐다 (저장한 뒤 이번에 한 번) */
|
||||
/** apply — 「받은편지함에 이미 있는 메일도 정리하기」를 골랐다 (저장한 뒤 이번에 한 번) */
|
||||
(e: 'save', rule: MailFilterRule, apply: boolean): void
|
||||
(e: 'remove'): void
|
||||
(e: 'cancel'): void
|
||||
@@ -124,7 +124,7 @@ const problems = ref<MailFilterProblems>(noFilterProblems(draft.conditions.lengt
|
||||
let base = baseOf()
|
||||
/** 창을 연 때의 칸 값 — 이것과 다르면 바꾼 것이다 (나가기 확인) */
|
||||
const opened = shallowRef<MailFilterDraft>(copyFilterDraft(draft))
|
||||
/** 「받은편지함에 이미 있는 메일에도 적용」을 골랐다 — 열 때마다 꺼진다 */
|
||||
/** 「받은편지함에 이미 있는 메일도 정리하기」를 골랐다 — 열 때마다 꺼진다 */
|
||||
const applyChosen = ref<boolean>(false)
|
||||
|
||||
const applyBlock = computed(() => filterApplyBlock(draft, props.mailboxes, props.applyBusy))
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// 바탕화면 알림 켜기 · 알림에 보일 것 · 시험 알림 (디자인 원본 SettingsNotify · MailRealtime "막혔을 때 · 지원하지 않을 때")
|
||||
// 바탕화면 알림 켜기 · 알림 내용 · 시험 알림 (디자인 원본 SettingsNotify · MailRealtime "막혔을 때 · 지원하지 않을 때")
|
||||
// - [x] 새 메일이 오면 바탕화면 알림 보내기(44) — 켜면 브라우저가 허락을 묻는다(누른 뒤에만). 칸 아래 안내(12.5px):
|
||||
// 묻기 전(회색) · 허용됨(초록 + 체크) · 막힘(칸을 끄고 빨강 + 경고) · 지원하지 않음(칸을 끄고 회색) — notifyEnableState
|
||||
// 허락하지 않았으면(막음 · 그냥 닫음) 칸은 비운 채 돌아간다. 묻는 동안은 칸을 끄지 않고 aria-disabled 로만 막는다(초점이 그대로 남게).
|
||||
// 막혀서(또는 지원하지 않아서) 칸이 꺼지면 초점을 안내 글로 옮긴다 — 왜 꺼졌는지 바로 읽게
|
||||
// - 알림에 보일 것: [x] 메일 제목도 보이기 — 알림을 끄면 흐리게(누를 수 없다)
|
||||
// - 알림 내용: [x] 메일 제목도 보이기 — 알림을 끄면 흐리게(누를 수 없다)
|
||||
// - [시험 알림 보내기](보조 단추 44 · 종 아이콘) — 지금 탭에서 바로 "ARA 웹메일" / "알림이 잘 옵니다. …" (알림을 끄면 누를 수 없다).
|
||||
// 못 띄우면 조용히 넘어가지 않는다 — 만들 수 없는 브라우저(휴대폰 Chrome 등)면 "지원하지 않음", 허락이 사라졌으면 그 안내로 바꾸고 초점을 옮긴다
|
||||
// 고정 id — #notify-enabled · #notify-enabled-note · #notify-show-subject · #notify-test
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
// 설정 › 알림의 "실시간 연결" (디자인 원본 SettingsNotify) — 이 탭이 보는 연결 상태 (stores/mail-realtime.store.ts)
|
||||
// ● 초록 "연결되어 있습니다 — 새 메일이 오면 바로 목록에 나타납니다." / 끊겼을 때 "연결할 수 없어 1분마다 확인합니다." (저절로 다시 잇는다)
|
||||
// ● 초록 "연결되어 있어 새 메일이 오면 바로 알 수 있습니다." / 끊겼을 때 "연결할 수 없어 1분마다 확인합니다." (저절로 다시 잇는다)
|
||||
// 끊긴 동안은 상태 줄 아래에 "연결될 때까지 바탕화면 알림은 오지 않습니다." (2026-10-28 화면 점검 반영)
|
||||
// 메일 화면에서는 끊겨도 알리지 않는다 — 여기서만 볼 수 있다 (디자인 README "5단계 › 실시간 반영")
|
||||
import { computed } from 'vue'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
// 부재중 자동 응답의 기간 (디자인 원본 SettingsVacation) — (•) 끌 때까지 계속 / ( ) 기간 정하기 → 시작 ~ 끝 날짜 칸(44)
|
||||
// "시작일 0시부터 끝나는 날 자정까지 답합니다 (이 브라우저의 시간대 기준)." 끈 동안은 묶음째 흐리게(쓴 값은 남긴다).
|
||||
// "시작하는 날 0시부터 끝나는 날 밤 12시까지 답합니다. 시각은 이 PC의 시간으로 셉니다." 끈 동안은 묶음째 흐리게(쓴 값은 남긴다).
|
||||
// 날짜는 <input type="date"> 값(YYYY-MM-DD) 그대로 주고받는다 — 시각으로 바꾸는 일은 mail-vacation.ts
|
||||
// 고정 id — #vacation-period-until-off · #vacation-period-range · #vacation-from · #vacation-to · #vacation-period-error
|
||||
import { ref } from 'vue'
|
||||
|
||||
Reference in New Issue
Block a user