목표: 코드 없이 엔터프라이즈급 UI를 만든다. 백엔드 데이터와 Fiori 화면을 완전히 연결한다.
시나리오: List Report + Object Page로 "출장비 승인 앱"의 완전한 풀스택 로컬 앱 완성.
소요 시간: 이론 2시간 + 실습 3시간
이론:
Fiori Elements 아키텍처와 Template 종류를 이해한다
CDS Annotation의 원리를 알고 자유자재로 사용할 수 있다
List Report와 Object Page의 자동 생성 원리를 안다
검색, 필터, 정렬을 UI에서 자동으로 생성하는 방법을 안다
실습:
Fiori 프로젝트 스캐폴딩 (SAP Fiori Generator)
List Report 생성 및 Annotation 커스터마이징
Object Page 추가 및 연관 데이터 표시
로컬에서 풀스택 시나리오 (데이터 입력 → 목록 조회 → 승인) 완동작
3일차 결과 Git 커밋
전통적인 SAP UI5 개발:
// 직접 Control 코딩으로 화면 만들기 (매우 번거로움)
var oTable = new sap.ui.table.Table({
columns: [
new sap.ui.table.Column({
label: new sap.m.Label({ text: "출장 제목" }),
template: new sap.m.Text({ text: "{title}" })
}),
// ... 계속 코드로 컬럼 정의
]
});
문제점:
// annotation은 데이터 모델에 메타데이터만 추가
annotate TravelService.TravelRequests with @(
UI: {
ListPageConfiguration: {
SelectionFields: ['status', 'requester'],
LineItem: [
{ Value: title, Label: '제목' },
{ Value: destination, Label: '목적지' },
{ Value: amount, Label: '금액', Criticality: #Neutral },
]
}
}
);
장점:

이 강좌: List Report + Object Page (완전한 CRUD 앱)
선택 기준:
// 실제 데이터는 안 건드림 — 표시만 추가
annotate Travel.TravelRequests with @(
// 이 블록들은 Annotation
// 데이터 스키마는 변하지 않음
);
entity TravelRequests {
// ← 이 부분은 변하지 않음
}
UI (UI 표시 방식)
├── ListPageConfiguration (목록 화면)
│ ├── SelectionFields (검색/필터)
│ ├── LineItem (목록 컬럼)
│ └── PresentationVariant (정렬, 기본값)
├── FieldGroup (그룹 표시)
├── Facets (Object Page 탭)
└── ...
Common (공통 설정)
├── Label (레이블)
├── Description (설명)
├── ValueList (드롭다운 목록)
└── ...
Measures (측정값)
├── Unit (단위)
├── NumberOfFractionalDigits (소수 자릿수)
└── ...
UI 흐름:
1. Fiori 앱 시작
↓
2. List Report 화면 로드
├─ SelectionFields로 검색/필터 폼 자동 생성
├─ LineItem으로 목록 테이블 자동 생성
└─ 기본 데이터 조회 (GET /TravelRequests)
↓
3. 사용자: 행 클릭
↓
4. Object Page 화면 로드 (ID 파라미터 포함)
├─ 상세 정보 로드 (GET /TravelRequests(ID))
├─ Facets로 탭 구성
├─ FieldGroup으로 필드 배치
└─ $expand로 연관 데이터 함께 로드
↓
5. 사용자: 수정 또는 Action 실행
↓
6. 백엔드: PATCH 또는 Custom Action 처리
↓
7. UI: 결과 반영 (자동)
터미널에서 필요 라이브러리 설치
npm install -g yo @sap/generator-fiori
BAS Terminal에서:
cd /home/user/projects/sap-btp-travel-expense
# Fiori 프로젝트 생성 (대화형)
yo @sap/fiori
대화형 질문 응답:
? What do you want to do?
→ Create a new Fiori project [선택]
? Which template do you want to use?
→ List Report Page [선택]
? Data Source
→ Use a Local CAP Project [선택]
? Select your CAP project folder
→ ./travel-expense-app [엔터]
? Which OData Service would you like to use?
→ /travel (TravelService) [선택]
? Which entity/table to display?
→ TravelRequests [선택]
? Do you want to add Object Page?
→ Yes [선택]
? Enter a project name
→ travel-expense-ui [엔터]
? Enter a module name
→ travel_expense_ui [엔터]
? Enter the namespace for your module
→ sap.example [변경 불필요 또는 custom 입력]
? Add empty i18n file?
→ Yes
? Configure advanced options?
→ No
생성 완료 후 구조:
travel-expense-app/
├── app/
│ └── travel-expense-ui/ ← 새로 생성된 Fiori 앱 (TypeScript 기반)
│ ├── webapp/
│ │ ├── i18n/ ← 다국어 지원 파일
│ │ ├── test/ ← 테스트 자동화 폴더
│ │ ├── Component.ts ← 앱 컴포넌트 정의
│ │ ├── manifest.json ← 앱 설정 메인 파일 (중요!)
│ │ ├── index.html ← 로컬 테스트용 페이지
│ │ └── annotations.cds ← UI 설정 (Local)
│ ├── package.json ← UI 프로젝트 의존성 관리
│ ├── tsconfig.json ← TypeScript 설정
│ └── ui5.yaml ← UI5 Tooling 서버/빌드 설정
├── db/ ← 데이터 모델 (CDS)
├── srv/ ← 서비스 정의 (CDS)
└── package.json (루트) ← CAP 프로젝트 전체 관리
cd app/travel_expense_ui
npm install
잘 안되면
npm install --legacy-peer-deps
(버전 체크 잠시 끄고...)
# UI5 버전 확인
npm list @sap/ux-ui5-tooling

srv/ 폴더에 새 파일 생성:
touch srv/annotations.cds
using TravelService from './travel-service';
// ════════════════════════════════════════════════════
// TravelRequests 엔티티 어노테이션
// ════════════════════════════════════════════════════
annotate TravelService.TravelRequests with @(
Capabilities.Insertable: true,
Capabilities.Deletable : true,
// ── List Report (검색/필터/목록) ──────────────────────────
UI: {
SelectionFields: [
status,
requester,
departureDate,
destination
],
LineItem: [
{ $Type: 'UI.DataFieldForAction', Action: 'TravelService.submit', Label: '제출', InvocationGrouping: #ChangeSet },
{ $Type: 'UI.DataFieldForAction', Action: 'TravelService.approve', Label: '승인', InvocationGrouping: #ChangeSet },
{ $Type: 'UI.DataFieldForAction', Action: 'TravelService.reject', Label: '반려', InvocationGrouping: #ChangeSet },
{ Value: title, Label: '제목', Importance: #High },
{ Value: destination, Label: '목적지', Importance: #High },
{ Value: requester, Label: '신청자', Importance: #Medium },
{ Value: totalAmount, Label: '금액 (KRW)', Importance: #High },
{
Value: status,
Label: '상태',
Criticality: (status = 'Approved' ? 3 : (status = 'Rejected' ? 1 : 2)),
CriticalityRepresentation: #WithIcon
},
{ Value: createdAt, Label: '신청 일자', Importance: #Low },
],
Identification: [
{ $Type: 'UI.DataFieldForAction', Action: 'submit', Label: '제출' },
{ $Type: 'UI.DataFieldForAction', Action: 'approve', Label: '승인' },
{ $Type: 'UI.DataFieldForAction', Action: 'reject', Label: '반려' }
],
PresentationVariant: {
SortOrder: [{ Property: ID, Descending: false }],
Visualizations: ['@UI.LineItem']
},
// ── Object Page (상세 화면) ────────────────────────────
HeaderInfo: {
TypeName: '출장 신청',
TypeNamePlural: '출장 신청 목록',
Title: { Value: title },
Description: { Value: destination }
},
Facets: [
{
$Type: 'UI.ReferenceFacet',
Label: '기본 정보',
Target: '@UI.FieldGroup#Main'
},
{
$Type: 'UI.ReferenceFacet',
Label: '비용 항목',
Target: 'items/@UI.LineItem'
}
],
FieldGroup#Main: {
Data: [
{ Value: title, Label: '출장 제목' },
{ Value: destination, Label: '목적지' },
{ Value: requester, Label: '신청자' },
{ Value: status, Label: '진행 상태' },
{ Value: departureDate, Label: '출발 일자' },
{ Value: returnDate, Label: '복귀 일자' },
{ Value: totalAmount, Label: '총 예산 (KRW)' }
]
}
}
) {
// 필드 레벨 어노테이션 (드롭다운 강제)
status @Common.Label : '상태'
@Common.ValueListWithFixedValues : true
@Common.ValueList : {
Label: '상태 선택',
CollectionPath: 'TravelRequests',
Parameters: [
{ $Type: 'Common.ValueListParameterInOut', LocalDataProperty: status, ValueListProperty: 'status' }
]
};
requester @Common.Label : '신청자';
departureDate @Common.Label : '출발 일자';
destination @Common.Label : '목적지';
};
// ════════════════════════════════════════════════════
// ExpenseItems 엔티티 어노테이션
// ════════════════════════════════════════════════════
annotate TravelService.ExpenseItems with @(
UI: {
LineItem: [
{ Value: category, Label: '항목 유형' },
{ Value: itemTitle, Label: '설명' },
{ Value: amount, Label: '금액' },
{ Value: expenseDate, Label: '발생일' }
]
}
);
---
### 실습 3. Fiori 앱 로컬 실행 및 통합 테스트
#### 3-1. CAP 서버와 Fiori 앱 동시 실행
**터미널 1 — CAP 서버 (백엔드):**
```bash
cd /home/user/projects/sap-btp-travel-expense/travel-expense-app
cds watch
터미널 2 — Fiori 앱 (프론트엔드):
윗부분 생략...
"scripts": {
"start": "fiori run --config ./ui5.yaml --open index.html",
"deploy-config": "npx -p @sap/ux-ui5-tooling fiori add deploy-config cf"
}
start 부분을 추가해 주세요.
cd /home/user/projects/sap-btp-travel-expense/app/travel_expense_ui
npm start
ui5.yaml 수정 -> 백앤드로 가는 통로 열어줌
# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json
specVersion: "4.0"
metadata:
name: sap.example.travelexpenseui
type: application
server:
customMiddleware:
- name: fiori-tools-proxy
afterMiddleware: compression
configuration:
ignoreCertErrors: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted
backend:
- path: /travel
url: http://127.0.0.1:4004
ui5:
path:
- /resources
- /test-resources
url: https://sapui5.hana.ondemand.com
- name: fiori-tools-appreload
afterMiddleware: compression
configuration:
port: 35729
path: webapp
delay: 300
- name: fiori-tools-preview
afterMiddleware: fiori-tools-appreload
configuration:
flp:
theme: sap_horizon
- name: ui5-tooling-transpile-middleware
afterMiddleware: compression
configuration:
debug: true
transformModulesToUI5:
overridesToOverride: true
excludePatterns:
- /Component-preload.js
builder:
customTasks:
- name: ui5-tooling-transpile-task
afterTask: replaceVersion
configuration:
debug: true
transformModulesToUI5:
overridesToOverride: true
포트 확인:
- CAP: http://localhost:4004
- Fiori: http://localhost:8080
Step 1: List Report 로드
브라우저: http://localhost:8080
↓
검색 폼 자동 표시:
- 상태 (드롭다운)
- 신청자 (텍스트)
- 출발일 (날짜)
- 목적지 (텍스트)
↓
목록 테이블 표시:
┌──────────┬──────────┬────────┬────────────┬──────────┐
│ 제목 │ 목적지 │ 신청자 │ 금액 │ 상태 │
├──────────┼──────────┼────────┼────────────┼──────────┤
│ 도쿄... │ 도쿄 │ 홍길동 │ 850,000 │ 제출됨 │
│ 싱가... │ 싱가포르 │ 김영희 │ 2,100,000│ 초안 │
└──────────┴──────────┴────────┴────────────┴──────────┘

Step 2: 목록에서 행 클릭 → Object Page
클릭한 행:
"도쿄 고객사 미팅" (ID: 11111111-1111...)
↓
Object Page 로드:
┌─ 기본 정보 ─┬─ 비용 항목 ─┬─ 승인 이력 ─┬─ 부서 ─┐
│ 필드들 │ 테이블 │ 테이블 │ 정보 │
└─────────────┴────────────┴─────────────┴─────┘
기본 정보 탭:
제목: 도쿄 고객사 미팅
목적지: 도쿄, 일본
신청자: 홍길동
이메일: hong@company.com
출발일: 2025-09-01
귀국일: 2025-09-03
총 출장비: 850,000 KRW
상태: 제출됨
비용 항목 탭:
┌──────────────────┬───────────┬─────────┐
│ 항목명 │ 카테고리 │ 금액 │
├──────────────────┼───────────┼─────────┤
│ 인천-도쿄 항공권 │ 교통비 │ 450,000 │
│ 도쿄 호텔 2박 │ 숙박비 │ 320,000 │
│ 현지 식비 │ 식비 │ 80,000 │
└──────────────────┴───────────┴─────────┘
승인 이력 탭:
┌───────────────────┬──────────────────────────┐
│ 처리 방식 │ 의견 │
├───────────────────┼──────────────────────────┤
│ 제출됨 │ 출장 신청이 제출되었습니다│
└───────────────────┴──────────────────────────┘

Step 3: 상태 변경 Action 실행
Object Page의 "승인" 버튼 클릭 (화면 상단 오른쪽):

Fiori가 자동으로 Custom Action UI 생성:
- Action: TravelRequests.approve()
- 입력 필드: comment (텍스트)
↓
승인 의견 입력 후 "승인" 클릭
↓
POST /travel/TravelRequests(ID)/approve
{
"comment": "출장 목적이 명확합니다. 승인합니다."
}
↓
응답:
{
"ID": "11111111-...",
"title": "도쿄 고객사 미팅",
"status": "Approved", // ← 변경됨
...
}
↓
Fiori UI 자동 갱신:
- 목록 테이블에서 상태 색상 변경 (주황 → 녹색)
- Object Page 상태 업데이트
- 승인 이력 탭에 새 항목 추가

제출이 안된 상태에서 승인을 요청할 경우 오류가 뜬다

=> 상단 제출 버튼을 눌러 제출부터 진행해 본다. (프로세스 순서가 있으니까...)
Fiori의 "Create" 버튼으로 새 출장비 신청 생성:
버튼 클릭
↓
Object Page (생성 모드) 로드
↓
필드 입력:
제목: [입력]
목적지: [입력]
출발일: [선택]
귀국일: [선택]
...
↓
"Save" 버튼 클릭
↓
POST /travel/TravelRequests
{
"title": "부산 파트너사 방문",
"destination": "부산",
...
}
↓
생성 성공 → List Report로 돌아옴
↓
새 항목이 목록에 표시됨

생성을 누르면 "오브젝트가 생성되었습니다" 라는 모달이 뜸.

시나리오: "2025-09 이후 출발하는 승인된 출장만 보기"
List Report의 SearchBox:
1. 상태: "Approved" 선택
2. 출발일: "2025-08-01" 이후 선택
3. "Go" 클릭
↓
Fiori → CAP → OData 쿼리 자동 생성:
GET /travel/TravelRequests?
$filter=status eq 'Approved' and departureDate ge 2025-08-01
&$orderby=createdAt desc
↓
필터링된 결과만 표시

cd /home/user/projects/sap-btp-travel-expense
# root package.json 업데이트 (선택)
git add .
git commit -m "day3: Fiori Elements UI (List Report + Object Page) + Annotation 적용"
git push origin main
# 다음 날을 위한 브랜치
git checkout -b day4-start
git push origin day4-start
git checkout main
// srv/annotations.cds
annotate TravelService.TravelRequests with @(
UI.HeaderInfo: {
TypeName: '출장 신청',
TypeNamePlural: '출장 신청들',
Title: { Value: title }
},
// 버튼들
UI.Identification: [
{ Value: 'submit', Label: '제출', Inline: true },
{ Value: 'approve', Label: '승인', Inline: true },
{ Value: 'reject', Label: '반려', Inline: true }
]
);
annotate TravelService.TravelRequests with {
totalAmount @(UI.Hidden: true); // 목록에서 숨김
createdAt @(UI.Hidden: false); // 항상 표시
};
// 또는 상태에 따라
annotate TravelService.TravelRequests with {
totalAmount @(Common.FieldControl: {
$edmType: 'Edm.EnumMember',
EnumMember: '{status} == "Approved" ? "ReadOnly" : "Editable"'
});
};
확인 항목:
[ ] BAS에서 yo @sap/fiori로 프로젝트 생성 완료
[ ] List Report 자동 생성 확인 (검색폼, 테이블)
[ ] Object Page 자동 생성 확인 (Facets, FieldGroup)
[ ] CAP 서버 + Fiori 앱 동시 실행 성공
[ ] 목록 조회, 항목 클릭, 상세 보기 작동
[ ] Create 버튼으로 새 항목 생성 가능
[ ] 필터/검색 기능 작동
[ ] Approve Action 실행 및 상태 변경 확인
[ ] 필터/정렬 자동 반영 확인
[ ] Git 커밋 완료
Q&A 주제 (강사에게 물어보기):
[ ] Fiori Elements와 Freestyle UI5의 선택 기준은?
[ ] Annotation의 복잡한 조건식은 어떻게 작성?
[ ] List Report의 성능 최적화 (대량 데이터)?
오늘 배운 것:
┌─────────────────────────────────────────────┐
│ Fiori Elements = Annotation으로 UI 생성 │
│ │
│ 3가지 주요 Annotation: │
│ - SelectionFields: 검색폼 │
│ - LineItem: 목록 컬럼 │
│ - Facets: Object Page 탭 │
│ │
│ CAP의 OData + Fiori Elements │
│ → 풀스택 엔터프라이즈 앱 자동 생성! │
└─────────────────────────────────────────────┘
지금까지의 성과:
┌─────────────────────────────────────────────┐
│ DAY1: CAP 백엔드 ✓ │
│ DAY2: 데이터 모델 고급화 ✓ │
│ DAY3: Fiori UI 완성 ✓ │
│ │
│ 현재: 로컬에서 완전한 풀스택 앱 완성! │
│ │
│ 다음: 클라우드에 배포 (DAY4) │
└─────────────────────────────────────────────┘
내일 할 것:
→ XSUAA (인증) 설정
→ MTA 빌드 (멀티-타겟 애플리케이션)
→ Cloud Foundry에 배포
→ BTP Cockpit에서 앱 확인
다음: [4일차] 클라우드 배포 (XSUAA + MTA + CF Deploy)
sqlite deploy
cds deploy --to sqlite:db/travel.sqlite
yo install
npm list -g yo @sap/generator-fiori --depth=0