구조화 목록 Structured list
구조화 목록은 유사하거나 관련된 콘텐츠 집합을 표현하기 위한 형식으로 목록에 제공된 데이터에 대한 상세 정보 탐색 수단 또는 관련 기능 실행 수단으로 활용된다. 사용자가 콘텐츠를 효율적으로 탐색하고 다음 행동을 빠르게 결정할 수 있도록 목록 내 정보는 상세 페이지에서 제공되는 복잡한 콘텐츠 중 핵심적이거나 흥미를 끌 수 있는 정보를 논리적 흐름에 따라 조직화하여 명확한 위계 구조를 반영해 제공해야 한다.
기본
<ul krdsStructuredList> 안에 항목마다 <li krdsStructuredItem>을 둡니다. 항목 안은 KRDS 영역 클래스 card-top, card-body, card-btm, card-btn을 붙인 요소로 나누고, 필요한 영역만 넣습니다. 기본은 한 줄에 항목 하나(전체 폭)이고, [full]="false"면 한 줄에 여러 항목을 둡니다(PC 3개, 태블릿 2개, 모바일 1개).
import { Component } from '@angular/core'
import { KrdsBadge, KrdsButton, KrdsStructuredItem, KrdsStructuredList } from '@krds.ui/angular'
@Component({
selector: 'demo-structured-list-basic',
imports: [KrdsBadge, KrdsButton, KrdsStructuredItem, KrdsStructuredList],
template: `
<ul krdsStructuredList>
<li krdsStructuredItem>
<div class="card-top">
<span krdsBadge type="bg-light" color="primary">뱃지</span>
</div>
<div class="card-body">
<a href="#" class="c-text">
<p class="c-tit"><span class="span">타이틀 영역</span></p>
<p class="c-txt">
간단한 설명이 들어가는 영역입니다. 최대 3줄까지 작성합니다. 간단한 설명이 들어가는 영역입니다. 간단한 설명이 들어가는
영역입니다.
</p>
<p class="c-date">
<strong class="key">신청 기간</strong>
<span class="value">2023.00.00-2024.00.00</span>
</p>
</a>
<div class="c-btn">
<button krdsButton variant="secondary" size="medium">신청하기</button>
</div>
</div>
<div class="card-btm">
<span class="tag">태그</span>
<span class="tag">태그</span>
</div>
<div class="card-btn">
<button krdsButton variant="tertiary" size="medium" text><i class="svg-icon ico-share" aria-hidden="true"></i> 공유하기</button>
<button krdsButton variant="tertiary" size="medium" text><i class="svg-icon ico-like" aria-hidden="true"></i> 찜하기</button>
</div>
</li>
</ul>
`
})
export default class StructuredListBasic {}뱃지만 있는 구조
import { Component } from '@angular/core'
import { KrdsBadge, KrdsStructuredItem, KrdsStructuredList } from '@krds.ui/angular'
@Component({
selector: 'demo-structured-list-simple-badge',
imports: [KrdsBadge, KrdsStructuredItem, KrdsStructuredList],
template: `
<ul krdsStructuredList>
<li krdsStructuredItem>
<div class="card-top">
<span krdsBadge type="bg-light" color="success">성공</span>
</div>
<div class="card-body">
<div class="c-text">
<p class="c-tit"><span class="span">간단한 제목</span></p>
</div>
</div>
</li>
</ul>
`
})
export default class StructuredListSimpleBadge {}표형
같은 목록을 표로 보여 주는 형태입니다(원본 KRDS v1.1.0). 목록 위 도구 막대(.search-list-top)에 전체선택·일괄 작업 버튼과 목록 표시 개수·정렬 기준을 두고, 아래에 표와 페이지네이션을 둡니다. 정렬 기준은 PC에서 버튼, 모바일에서 셀렉트로 바뀝니다. 표는 krds-table의 셀 템플릿(<ng-template krdsCell="열 이름">)으로 행마다 체크박스와 다운로드 버튼을 넣고, 도구 막대의 체크박스·버튼·셀렉트와 페이지네이션도 컴포넌트로 조합합니다. 전체선택은 지금 페이지의 행 기준입니다.
넓은 화면을 전제로 한 형태라 예제를 1200px 폭의 별도 화면(iframe)에 띄웁니다.
import { Component, computed, signal } from '@angular/core'
import { FormsModule } from '@angular/forms'
import {
KrdsButton,
KrdsCheckArea,
KrdsCheckbox,
KrdsControl,
KrdsPagination,
KrdsSelect,
KrdsTable,
KrdsTableCell,
KrdsTableHeader,
type KrdsTableColumn
} from '@krds.ui/angular'
interface Notice {
id: number
type: string
title: string
summary: string
date: string
views: number
}
@Component({
selector: 'demo-structured-list-table-type',
imports: [
FormsModule,
KrdsButton,
KrdsCheckArea,
KrdsCheckbox,
KrdsControl,
KrdsPagination,
KrdsSelect,
KrdsTable,
KrdsTableCell,
KrdsTableHeader
],
template: `
<div class="krds-structured-list-table">
<div class="search-list-top">
<div class="sch-left">
<div krdsCheckArea>
<krds-checkbox>
<input type="checkbox" krdsControl id="table-type-chk-all" [ngModel]="allChecked()" (ngModelChange)="toggleAll($event)" />
전체선택
</krds-checkbox>
</div>
<ul class="side-line-ul">
<li>
<button krdsButton text size="medium" [disabled]="!selected().length">
<i class="svg-icon ico-down" aria-hidden="true"></i> 선택 다운로드
</button>
</li>
<li>
<button krdsButton text size="medium"><i class="svg-icon ico-down" aria-hidden="true"></i> 전체 다운로드</button>
</li>
</ul>
</div>
<ul class="sch-sort">
<li>
<strong class="sort-label"><label for="table-type-size">목록 표시 개수</label></strong>
<select krdsSelect sort id="table-type-size" [ngModel]="size()" (ngModelChange)="size.set($event); page.set(1)">
@for (option of sizeOptions; track option.value) {
<option [ngValue]="option.value">{{ option.label }}</option>
}
</select>
</li>
<li>
<strong class="sort-label"><label for="table-type-sort">정렬기준</label></strong>
<!-- PC는 버튼, 모바일은 셀렉트로 정렬 기준을 고른다 -->
<div class="w-sort-btn" role="group" aria-label="정렬기준">
@for (option of sortOptions; track option.value) {
<button
type="button"
[class.active]="sort() === option.value"
[attr.aria-pressed]="sort() === option.value"
(click)="setSort(option.value)"
>
{{ option.label }}
</button>
}
</div>
<div class="m-sort-btn">
<select krdsSelect sort id="table-type-sort" [ngModel]="sort()" (ngModelChange)="setSort($event)">
@for (option of sortOptions; track option.value) {
<option [value]="option.value">{{ option.label }}</option>
}
</select>
</div>
</li>
</ul>
</div>
<krds-table
caption="게시물 목록: 선택, 유형, 제목, 내용, 다운로드, 게시일로 구성"
tableClass="col data"
[columns]="columns"
[rows]="rows()"
>
<ng-template krdsCell="select" [krdsCellOf]="rows()" let-row>
<krds-checkbox>
<input
type="checkbox"
krdsControl
[id]="'table-type-chk-' + row.id"
[ngModel]="selected().includes(row.id)"
(ngModelChange)="toggleRow(row.id, $event)"
/>
<span class="sr-only">{{ row.title }} 선택</span>
</krds-checkbox>
</ng-template>
<ng-template krdsHeader="download"><span class="sr-only">다운로드</span></ng-template>
<ng-template krdsCell="download" [krdsCellOf]="rows()" let-row>
<button krdsButton text size="medium">
<i class="svg-icon ico-down" aria-hidden="true"></i> 다운로드<span class="sr-only">: {{ row.title }}</span>
</button>
</ng-template>
</krds-table>
<nav krdsPagination label="게시물 목록 페이지" [max]="maxPage()" [(page)]="page"></nav>
</div>
`
})
export default class StructuredListTableType {
readonly items: Notice[] = [
{
id: 1,
type: '공지',
title: '2026년 하반기 민원 서비스 개편 안내',
summary: '민원 신청 화면이 새 디자인 시스템으로 바뀝니다.',
date: '2026.10.02',
views: 812
},
{
id: 2,
type: '보도자료',
title: '디지털 정부 서비스 만족도 조사 결과',
summary: '전년 대비 만족도가 4.2점 올랐습니다.',
date: '2026.09.28',
views: 1540
},
{
id: 3,
type: '고시',
title: '전자문서 보존 기간 기준 개정 고시',
summary: '보존 기간 산정 기준이 일부 바뀝니다.',
date: '2026.09.15',
views: 402
},
{
id: 4,
type: '공지',
title: '추석 연휴 고객센터 운영 시간 안내',
summary: '연휴 기간 전화 상담은 오전에만 운영합니다.',
date: '2026.09.10',
views: 2210
},
{
id: 5,
type: '보도자료',
title: '공공 누리집 접근성 점검 결과 발표',
summary: '점검 대상 누리집의 91%가 기준을 충족했습니다.',
date: '2026.08.30',
views: 967
},
{
id: 6,
type: '공지',
title: '본인 인증 수단 추가 안내',
summary: '간편 인증으로 민원을 신청할 수 있습니다.',
date: '2026.08.21',
views: 1325
},
{
id: 7,
type: '고시',
title: '행정정보 공동이용 대상 정보 고시',
summary: '공동이용 대상 정보 3종이 추가됩니다.',
date: '2026.08.12',
views: 288
}
]
readonly columns: KrdsTableColumn<Notice>[] = [
{ name: 'select', label: '선택', headerStyle: 'width: 6%' },
{ name: 'type', label: '유형', field: 'type', headerStyle: 'width: 10%' },
{ name: 'title', label: '제목', field: 'title', headerStyle: 'width: 28%' },
{ name: 'summary', label: '내용', field: 'summary' },
{ name: 'download', label: '다운로드', headerStyle: 'width: 12%' },
{ name: 'date', label: '게시일', field: 'date', headerStyle: 'width: 12%' }
]
readonly sortOptions = [
{ value: 'relevance', label: '관련도순' },
{ value: 'latest', label: '최신순' },
{ value: 'popular', label: '인기순' }
]
readonly sizeOptions = [
{ value: 5, label: '5개' },
{ value: 10, label: '10개' }
]
readonly sort = signal('relevance')
readonly size = signal(5)
readonly page = signal(1)
readonly selected = signal<number[]>([])
readonly sorted = computed(() => {
if (this.sort() === 'latest') return [...this.items].sort((a, b) => b.date.localeCompare(a.date))
if (this.sort() === 'popular') return [...this.items].sort((a, b) => b.views - a.views)
return this.items
})
readonly maxPage = computed(() => Math.ceil(this.items.length / this.size()))
readonly rows = computed(() => this.sorted().slice((this.page() - 1) * this.size(), this.page() * this.size()))
// 전체선택은 지금 페이지의 행 기준
readonly allChecked = computed(() => this.rows().every(row => this.selected().includes(row.id)))
// 정렬 기준을 바꾸면 첫 페이지로
setSort(value: string) {
this.sort.set(value)
this.page.set(1)
}
toggleAll(checked: boolean) {
const ids = this.rows().map(row => row.id)
this.selected.update(selected => (checked ? [...new Set([...selected, ...ids])] : selected.filter(id => !ids.includes(id))))
}
toggleRow(id: number, checked: boolean) {
this.selected.update(selected => (checked ? [...selected, id] : selected.filter(item => item !== id)))
}
}