
もちろん。いま作ったファイルは、大きく分けると「AWS構成」「バックエンド」「フロントエンド」「設計書」「テスト」「ローカル確認用」です。
全体説明README.md
プロジェクトの説明書です。アプリの概要、AWS構成、ローカルでの起動方法、テスト方法、SAMでのデプロイ手順を書いています。
template.yaml
AWS SAM用の構成ファイルです。
ここで API Gateway、Lambda、DynamoDB を定義しています。AWSにデプロイすると、このファイルをもとに必要なリソースが作られます。
package.json
Node.jsプロジェクトの設定ファイルです。npm test や npm start で何を実行するかを定義しています。今回は外部ライブラリを増やさない最小構成です。
.gitignore
Gitで管理しないファイルを指定するものです。node_modules/ や .aws-sam/ など、生成物や環境依存ファイルを除外しています。
バックエンドsrc/diagnosis.mjs
Lambdaで動くメイン処理です。
主な役割は以下です。
- 回答から5種類のオリジナルタイプを判定する
- 名前、タイプ、診断日時をDynamoDBに保存する
- 保存済みの人の中から相性のよいタイプの人を探す
POST /diagnosesとGET /diagnosesのAPI処理を行う
つまり、アプリの「診断ロジック」と「保存処理」の中心です。
フロントエンドfrontend/index.html
画面の骨組みです。
名前入力欄、質問表示エリア、診断ボタン、結果表示エリアなどを置いています。
frontend/styles.css
画面デザイン用のCSSです。
レイアウト、色、ボタン、質問カード、スマホ対応などを定義しています。
frontend/app.js
画面側の動きを担当します。
主な役割は以下です。
- 10問の質問を画面に表示する
- 回答数をカウントする
- 診断ボタンを押したときに回答を集める
- APIに送信する
- ローカル確認時は
localStorageに仮保存する - 結果と相性のよい人を画面に表示する
AWSに接続する場合は、この中の API_BASE_URL にAPI GatewayのURLを入れます。
ローカル確認用server.cjs
フロント画面をローカルで表示するための簡易Webサーバーです。node server.cjs 4173 を実行すると、frontend/index.html をブラウザで見られるようになります。
テストtests/diagnosis.test.mjs
バックエンドの診断ロジック用テストです。
確認している内容は主に2つです。
- 回答から正しいタイプが判定されるか
- 自分自身を除外して、相性のよいタイプの人だけが表示されるか
設計書docs/requirements.md
要件定義メモです。
このアプリで「やること」「やらないこと」、機能要件、非機能要件、V字開発での成果物例を書いています。
docs/basic-design.md
基本設計メモです。
画面設計、API設計、DynamoDB設計、タイプ一覧、相性ルール、テスト方針を書いています。
ざっくり言うと、frontend/ が画面、src/ がLambda、template.yaml がAWS構成、docs/ が研修用の設計資料、tests/ がロジック確認用です。
frontrnd
index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sync Type Finder</title>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<main class="app-shell">
<section class="diagnosis-panel" aria-labelledby="app-title">
<div class="title-row">
<div>
<p class="eyebrow">同期タイプ診断</p>
<h1 id="app-title">Sync Type Finder</h1>
</div>
<div class="progress" aria-live="polite">
<span id="answered-count">0</span>/<span id="question-count">10</span>
</div>
</div>
<form id="diagnosis-form">
<label class="name-field">
<span>名前</span>
<input id="name-input" name="name" maxlength="30" autocomplete="name" required placeholder="例: 佐藤">
</label>
<div id="questions" class="questions"></div>
<button class="submit-button" type="submit">診断する</button>
<p id="form-message" class="form-message" role="status"></p>
</form>
</section>
<section id="result-panel" class="result-panel" hidden aria-live="polite">
<p class="eyebrow">診断結果</p>
<h2 id="result-title"></h2>
<p id="result-description" class="result-description"></p>
<div class="match-box">
<p class="match-label">相性のよい同期</p>
<p id="match-message" class="match-message"></p>
</div>
<button id="retry-button" class="secondary-button" type="button">もう一度診断する</button>
</section>
</main>
<script src="./app.js" type="module"></script>
</body>
</html>
styles.css
:root {
color-scheme: light;
--ink: #17202a;
--muted: #627184;
--line: #d7dde5;
--panel: #ffffff;
--page: #f5f7f8;
--accent: #256f7a;
--accent-strong: #174f58;
--warm: #c86b3c;
--focus: #f0b429;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", system-ui, sans-serif;
background: var(--page);
color: var(--ink);
}
button,
input {
font: inherit;
}
.app-shell {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
padding: 32px 0;
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.65fr);
gap: 24px;
align-items: start;
}
.diagnosis-panel,
.result-panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 24px;
box-shadow: 0 12px 30px rgba(23, 32, 42, 0.08);
}
.title-row {
display: flex;
align-items: start;
justify-content: space-between;
gap: 16px;
border-bottom: 1px solid var(--line);
padding-bottom: 18px;
margin-bottom: 20px;
}
.eyebrow {
color: var(--warm);
font-size: 0.86rem;
font-weight: 700;
margin: 0 0 4px;
}
h1,
h2 {
margin: 0;
letter-spacing: 0;
}
h1 {
font-size: clamp(1.8rem, 2.5rem, 2.5rem);
}
h2 {
font-size: 1.75rem;
}
.progress {
min-width: 72px;
text-align: center;
color: var(--accent-strong);
font-weight: 800;
border: 1px solid #b8d4d8;
background: #eef8f9;
border-radius: 999px;
padding: 8px 12px;
}
.name-field {
display: grid;
gap: 8px;
margin-bottom: 22px;
font-weight: 700;
}
.name-field input {
width: 100%;
min-height: 44px;
border: 1px solid var(--line);
border-radius: 6px;
padding: 10px 12px;
}
.name-field input:focus,
.question-option input:focus-visible + span,
button:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.questions {
display: grid;
gap: 16px;
}
.question-card {
border: 1px solid var(--line);
border-radius: 8px;
padding: 16px;
}
.question-title {
margin: 0 0 12px;
font-weight: 800;
}
.options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.question-option input {
position: absolute;
opacity: 0;
}
.question-option span {
display: block;
min-height: 48px;
border: 1px solid var(--line);
border-radius: 6px;
padding: 12px;
cursor: pointer;
background: #fbfcfd;
}
.question-option input:checked + span {
border-color: var(--accent);
background: #e8f4f5;
color: var(--accent-strong);
font-weight: 800;
}
.submit-button,
.secondary-button {
min-height: 46px;
border: 0;
border-radius: 6px;
padding: 0 18px;
cursor: pointer;
font-weight: 800;
}
.submit-button {
width: 100%;
margin-top: 20px;
background: var(--accent);
color: #fff;
}
.submit-button:hover {
background: var(--accent-strong);
}
.secondary-button {
background: #ecf0f3;
color: var(--ink);
}
.form-message {
min-height: 24px;
color: #9b2c2c;
font-weight: 700;
}
.result-panel {
position: sticky;
top: 24px;
}
.result-panel[hidden] {
display: none;
}
.result-description {
color: var(--muted);
line-height: 1.7;
}
.match-box {
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
padding: 18px 0;
margin: 18px 0;
}
.match-label {
margin: 0 0 6px;
color: var(--muted);
font-size: 0.9rem;
font-weight: 700;
}
.match-message {
margin: 0;
font-size: 1.2rem;
font-weight: 800;
line-height: 1.5;
}
@media (max-width: 860px) {
.app-shell {
grid-template-columns: 1fr;
width: min(100% - 20px, 680px);
padding: 16px 0;
}
.diagnosis-panel,
.result-panel {
padding: 18px;
}
.result-panel {
position: static;
}
}
@media (max-width: 560px) {
.title-row,
.options {
grid-template-columns: 1fr;
}
.title-row {
display: grid;
}
.progress {
width: max-content;
}
}
app.js
const API_BASE_URL = "";
const typeDescriptions = {
SPARK: "新しいことに素早く反応し、場を前に進めるタイプです。",
HARBOR: "周囲をよく見て、安心して話せる空気を作るタイプです。",
ROOT: "物事の土台を整え、着実に形にしていくタイプです。",
CRAFT: "細部を観察し、品質や完成度を高めるタイプです。",
BLOOM: "人の良さや可能性を見つけ、関係を広げるタイプです。",
};
const questions = [
{
text: "新しい課題が始まったとき、最初にやりたいことは?",
options: [
{ text: "まず動くものを作る", scores: { SPARK: 2, CRAFT: 1 } },
{ text: "目的と前提を整理する", scores: { ROOT: 2, HARBOR: 1 } },
],
},
{
text: "チームで話し合うときに意識しやすいことは?",
options: [
{ text: "意見を出して流れを作る", scores: { SPARK: 2, BLOOM: 1 } },
{ text: "全員が話しやすい状態にする", scores: { HARBOR: 2, BLOOM: 1 } },
],
},
{
text: "作業で気持ちよく感じる瞬間は?",
options: [
{ text: "チェックリストが順番に埋まる", scores: { ROOT: 2, CRAFT: 1 } },
{ text: "小さな改善で使いやすくなる", scores: { CRAFT: 2, HARBOR: 1 } },
],
},
{
text: "困っている同期がいたらどうする?",
options: [
{ text: "すぐ声をかけて一緒に考える", scores: { BLOOM: 2, HARBOR: 1 } },
{ text: "原因を切り分けて道筋を示す", scores: { ROOT: 2, CRAFT: 1 } },
],
},
{
text: "発表前に気になることは?",
options: [
{ text: "聞き手に伝わる熱量があるか", scores: { SPARK: 2, BLOOM: 1 } },
{ text: "説明の抜け漏れがないか", scores: { CRAFT: 2, ROOT: 1 } },
],
},
{
text: "予定より遅れたときに取りやすい行動は?",
options: [
{ text: "優先順位を決め直す", scores: { ROOT: 2, SPARK: 1 } },
{ text: "周囲に状況を共有する", scores: { HARBOR: 2, BLOOM: 1 } },
],
},
{
text: "レビューで見つけたいものは?",
options: [
{ text: "ユーザーが迷いそうな点", scores: { HARBOR: 2, BLOOM: 1 } },
{ text: "仕様と実装のずれ", scores: { CRAFT: 2, ROOT: 1 } },
],
},
{
text: "アイデア出しで得意な役割は?",
options: [
{ text: "たくさん案を出す", scores: { SPARK: 2, BLOOM: 1 } },
{ text: "案を整理して選びやすくする", scores: { ROOT: 2, HARBOR: 1 } },
],
},
{
text: "完成に近づいたとき、最後に見たいところは?",
options: [
{ text: "体験として自然に使えるか", scores: { HARBOR: 2, CRAFT: 1 } },
{ text: "細かい表示や文言が整っているか", scores: { CRAFT: 2, ROOT: 1 } },
],
},
{
text: "同期との関わりで大事にしたいことは?",
options: [
{ text: "互いの良さを見つける", scores: { BLOOM: 2, HARBOR: 1 } },
{ text: "一緒に成果を出す", scores: { SPARK: 1, ROOT: 2 } },
],
},
];
const form = document.querySelector("#diagnosis-form");
const questionsRoot = document.querySelector("#questions");
const answeredCount = document.querySelector("#answered-count");
const questionCount = document.querySelector("#question-count");
const formMessage = document.querySelector("#form-message");
const resultPanel = document.querySelector("#result-panel");
const resultTitle = document.querySelector("#result-title");
const resultDescription = document.querySelector("#result-description");
const matchMessage = document.querySelector("#match-message");
const retryButton = document.querySelector("#retry-button");
questionCount.textContent = String(questions.length);
renderQuestions();
updateAnsweredCount();
form.addEventListener("change", updateAnsweredCount);
form.addEventListener("submit", handleSubmit);
retryButton.addEventListener("click", () => {
resultPanel.hidden = true;
form.reset();
updateAnsweredCount();
document.querySelector("#name-input").focus();
});
function renderQuestions() {
questionsRoot.innerHTML = questions.map((question, questionIndex) => {
const options = question.options.map((option, optionIndex) => {
const id = `q${questionIndex}-${optionIndex}`;
return `
<label class="question-option" for="${id}">
<input id="${id}" type="radio" name="q${questionIndex}" value="${optionIndex}" required>
<span>${escapeHtml(option.text)}</span>
</label>
`;
}).join("");
return `
<fieldset class="question-card">
<legend class="question-title">${questionIndex + 1}. ${escapeHtml(question.text)}</legend>
<div class="options">${options}</div>
</fieldset>
`;
}).join("");
}
async function handleSubmit(event) {
event.preventDefault();
formMessage.textContent = "";
const name = document.querySelector("#name-input").value.trim();
const answers = collectAnswers();
if (answers.length !== questions.length) {
formMessage.textContent = "すべての質問に回答してください。";
return;
}
const submitButton = form.querySelector("button[type='submit']");
submitButton.disabled = true;
submitButton.textContent = "診断中...";
try {
const payload = await submitDiagnosis({ name, answers });
showResult(payload);
} catch (error) {
formMessage.textContent = error.message;
} finally {
submitButton.disabled = false;
submitButton.textContent = "診断する";
}
}
function collectAnswers() {
return questions.flatMap((question, questionIndex) => {
const checked = form.querySelector(`input[name='q${questionIndex}']:checked`);
if (!checked) return [];
return [question.options[Number(checked.value)].scores];
});
}
async function submitDiagnosis(payload) {
if (!API_BASE_URL) {
return submitDiagnosisLocally(payload);
}
const response = await fetch(`${API_BASE_URL}/diagnoses`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error("診断結果を保存できませんでした。");
}
return response.json();
}
function submitDiagnosisLocally(payload) {
const result = calculateType(payload.answers);
const records = JSON.parse(localStorage.getItem("sync-type-records") ?? "[]");
const record = {
name: payload.name,
type: result.type,
label: result.label,
createdAt: new Date().toISOString(),
};
records.push(record);
localStorage.setItem("sync-type-records", JSON.stringify(records));
return {
name: payload.name,
result,
matches: findLocalMatches(records, payload.name, result.type),
};
}
function calculateType(answers) {
const scores = { SPARK: 0, HARBOR: 0, ROOT: 0, CRAFT: 0, BLOOM: 0 };
for (const answer of answers) {
for (const [type, value] of Object.entries(answer)) {
scores[type] += value;
}
}
const [type] = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
return {
type,
label: typeLabels[type],
compatibleTypes: compatibility[type],
scores,
};
}
function findLocalMatches(records, ownName, ownType) {
return records
.filter((record) => compatibility[ownType].includes(record.type))
.filter((record) => record.name.trim().toLowerCase() !== ownName.trim().toLowerCase())
.slice(0, 5);
}
function showResult(payload) {
resultTitle.textContent = `${payload.name}さんは${payload.result.label}タイプ`;
resultDescription.textContent = typeDescriptions[payload.result.type];
if (payload.matches.length > 0) {
const names = payload.matches.map((match) => `${match.name}さん`).join("、");
matchMessage.textContent = `あなたは${names}と相性がいい!`;
} else {
matchMessage.textContent = "まだ相性のよい同期が登録されていません。次の人の診断を待ちましょう。";
}
resultPanel.hidden = false;
resultPanel.scrollIntoView({ behavior: "smooth", block: "start" });
}
function updateAnsweredCount() {
const answered = questions.filter((_, index) => form.querySelector(`input[name='q${index}']:checked`)).length;
answeredCount.textContent = String(answered);
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
const typeLabels = {
SPARK: "スパーク",
HARBOR: "ハーバー",
ROOT: "ルート",
CRAFT: "クラフト",
BLOOM: "ブルーム",
};
const compatibility = {
SPARK: ["HARBOR", "ROOT"],
HARBOR: ["SPARK", "CRAFT"],
ROOT: ["SPARK", "BLOOM"],
CRAFT: ["HARBOR", "BLOOM"],
BLOOM: ["ROOT", "CRAFT"],
};
src
diagnosis.mjs
import crypto from "node:crypto";
const TYPE_COMPATIBILITY = {
SPARK: ["HARBOR", "ROOT"],
HARBOR: ["SPARK", "CRAFT"],
ROOT: ["SPARK", "BLOOM"],
CRAFT: ["HARBOR", "BLOOM"],
BLOOM: ["ROOT", "CRAFT"],
};
const TYPE_LABELS = {
SPARK: "スパーク",
HARBOR: "ハーバー",
ROOT: "ルート",
CRAFT: "クラフト",
BLOOM: "ブルーム",
};
const responseHeaders = {
"content-type": "application/json; charset=utf-8",
"access-control-allow-origin": "*",
};
export function calculateType(answers) {
if (!Array.isArray(answers) || answers.length === 0) {
throw new Error("answers must be a non-empty array");
}
const scores = {
SPARK: 0,
HARBOR: 0,
ROOT: 0,
CRAFT: 0,
BLOOM: 0,
};
for (const answer of answers) {
if (!answer || typeof answer !== "object") {
throw new Error("answer must be an object");
}
for (const [type, value] of Object.entries(answer)) {
if (!(type in scores) || !Number.isInteger(value)) {
throw new Error("answer contains an invalid score");
}
scores[type] += value;
}
}
const sorted = Object.entries(scores).sort((a, b) => {
if (b[1] !== a[1]) return b[1] - a[1];
return Object.keys(scores).indexOf(a[0]) - Object.keys(scores).indexOf(b[0]);
});
const type = sorted[0][0];
return {
type,
label: TYPE_LABELS[type],
compatibleTypes: TYPE_COMPATIBILITY[type],
scores,
};
}
export function findMatches(records, ownName, ownType) {
const compatibleTypes = TYPE_COMPATIBILITY[ownType] ?? [];
return records
.filter((record) => compatibleTypes.includes(record.type))
.filter((record) => normalizeName(record.name) !== normalizeName(ownName))
.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)))
.slice(0, 5)
.map((record) => ({
name: record.name,
type: record.type,
label: TYPE_LABELS[record.type] ?? record.type,
}));
}
export async function handler(event) {
try {
if (event.requestContext?.http?.method === "POST") {
return await handleCreate(event);
}
if (event.requestContext?.http?.method === "GET") {
return await handleList(event);
}
return json(405, { message: "Method not allowed" });
} catch (error) {
console.error(error);
if (error instanceof SyntaxError || error.message.includes("must be")) {
return json(400, { message: error.message });
}
return json(500, { message: "Internal server error" });
}
}
async function handleCreate(event) {
const body = JSON.parse(event.body ?? "{}");
const name = sanitizeName(body.name);
const result = calculateType(body.answers);
const now = new Date().toISOString();
const id = crypto.randomUUID();
const item = {
id,
name,
type: result.type,
createdAt: now,
};
await putItem(item);
const allRecords = await scanRecords();
return json(201, {
id,
name,
result,
matches: findMatches(allRecords, name, result.type),
});
}
async function handleList(event) {
const type = event.queryStringParameters?.type;
const records = await scanRecords();
const filteredRecords = type
? records.filter((record) => record.type === type)
: records;
return json(200, {
items: filteredRecords.map((record) => ({
name: record.name,
type: record.type,
label: TYPE_LABELS[record.type] ?? record.type,
createdAt: record.createdAt,
})),
});
}
function sanitizeName(value) {
const name = String(value ?? "").trim();
if (name.length < 1 || name.length > 30) {
throw new Error("name must be 1 to 30 characters");
}
return name;
}
function normalizeName(name) {
return String(name ?? "").trim().toLowerCase();
}
function json(statusCode, body) {
return {
statusCode,
headers: responseHeaders,
body: JSON.stringify(body),
};
}
async function putItem(item) {
const tableName = process.env.TABLE_NAME;
if (!tableName) {
throw new Error("TABLE_NAME is not set");
}
const { DynamoDBClient } = await import("@aws-sdk/client-dynamodb");
const { PutItemCommand } = await import("@aws-sdk/client-dynamodb");
const client = new DynamoDBClient({});
await client.send(new PutItemCommand({
TableName: tableName,
Item: {
id: { S: item.id },
name: { S: item.name },
type: { S: item.type },
createdAt: { S: item.createdAt },
},
}));
}
async function scanRecords() {
const tableName = process.env.TABLE_NAME;
if (!tableName) {
throw new Error("TABLE_NAME is not set");
}
const { DynamoDBClient } = await import("@aws-sdk/client-dynamodb");
const { ScanCommand } = await import("@aws-sdk/client-dynamodb");
const client = new DynamoDBClient({});
const result = await client.send(new ScanCommand({ TableName: tableName }));
return (result.Items ?? []).map((item) => ({
id: item.id?.S,
name: item.name?.S,
type: item.type?.S,
createdAt: item.createdAt?.S,
}));
}
素の
server.cjs
const http = require("node:http");
const fs = require("node:fs");
const path = require("node:path");
const port = Number(process.argv[2] ?? 4173);
const root = path.join(__dirname, "frontend");
const mimeTypes = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
};
const server = http.createServer((request, response) => {
const urlPath = new URL(request.url, `http://${request.headers.host}`).pathname;
const safePath = path.normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
const filePath = path.join(root, safePath === "/" ? "index.html" : safePath);
if (!filePath.startsWith(root)) {
response.writeHead(403);
response.end("Forbidden");
return;
}
fs.readFile(filePath, (error, content) => {
if (error) {
response.writeHead(404);
response.end("Not found");
return;
}
response.writeHead(200, {
"content-type": mimeTypes[path.extname(filePath)] ?? "application/octet-stream",
});
response.end(content);
});
});
server.listen(port, "127.0.0.1", () => {
console.log(`Server running at http://127.0.0.1:${port}/index.html`);
});
template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Minimal MBTI-like diagnosis app for new engineer training.
Globals:
Function:
Runtime: nodejs20.x
Timeout: 10
MemorySize: 128
Architectures:
- x86_64
Resources:
DiagnosisTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
DiagnosisApi:
Type: AWS::Serverless::HttpApi
Properties:
CorsConfiguration:
AllowOrigins:
- "*"
AllowMethods:
- GET
- POST
- OPTIONS
AllowHeaders:
- content-type
DiagnosisFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: diagnosis.handler
Environment:
Variables:
TABLE_NAME: !Ref DiagnosisTable
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref DiagnosisTable
Events:
SubmitDiagnosis:
Type: HttpApi
Properties:
ApiId: !Ref DiagnosisApi
Path: /diagnoses
Method: POST
ListByType:
Type: HttpApi
Properties:
ApiId: !Ref DiagnosisApi
Path: /diagnoses
Method: GET
Outputs:
ApiUrl:
Description: API Gateway endpoint URL
Value: !Sub "https://${DiagnosisApi}.execute-api.${AWS::Region}.amazonaws.com"
tests/diagnosis.test.mjs
import test from "node:test";
import assert from "node:assert/strict";
import { calculateType, findMatches } from "../src/diagnosis.mjs";
test("calculateType returns the strongest original type", () => {
const result = calculateType([
{ SPARK: 2, CRAFT: 1 },
{ SPARK: 2, BLOOM: 1 },
{ ROOT: 2, SPARK: 1 },
]);
assert.equal(result.type, "SPARK");
assert.equal(result.label, "スパーク");
assert.deepEqual(result.compatibleTypes, ["HARBOR", "ROOT"]);
});
test("findMatches excludes self and returns compatible people", () => {
const matches = findMatches([
{ name: "佐藤", type: "SPARK", createdAt: "2026-06-01T00:00:00.000Z" },
{ name: "田中", type: "HARBOR", createdAt: "2026-06-01T00:00:01.000Z" },
{ name: "鈴木", type: "ROOT", createdAt: "2026-06-01T00:00:02.000Z" },
{ name: "山田", type: "BLOOM", createdAt: "2026-06-01T00:00:03.000Z" },
], "佐藤", "SPARK");
assert.deepEqual(matches.map((match) => match.name), ["田中", "鈴木"]);
});
package.json
{
"name": "sync-type-finder",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"test": "node --test",
"start": "node server.cjs 4173"
}
}
basic-design.md
# 基本設計メモ
## 画面
### 診断画面
- 名前入力欄
- 10問の二択質問
- 回答数表示
- 診断ボタン
### 結果画面
- 診断者名
- 診断タイプ
- タイプ説明
- 相性のよい登録者名
## API
### POST /diagnoses
診断結果を保存し、相性候補を返す。
Request:
```json
{
"name": "佐藤",
"answers": [
{ "SPARK": 2, "CRAFT": 1 }
]
}
```
Response:
```json
{
"id": "uuid",
"name": "佐藤",
"result": {
"type": "SPARK",
"label": "スパーク",
"compatibleTypes": ["HARBOR", "ROOT"],
"scores": {
"SPARK": 10,
"HARBOR": 2,
"ROOT": 4,
"CRAFT": 3,
"BLOOM": 1
}
},
"matches": [
{ "name": "田中", "type": "HARBOR", "label": "ハーバー" }
]
}
```
### GET /diagnoses
保存済み診断者を取得する。動作確認や結合テスト用。
## DynamoDB
Table: `DiagnosisTable`
| 属性 | 型 | 内容 |
| --- | --- | --- |
| id | String | UUID、パーティションキー |
| name | String | 診断者名 |
| type | String | 診断タイプ |
| createdAt | String | ISO8601形式の診断日時 |
30人程度のため、相性候補取得はScanで実装する。件数が増えるサービスではGSIやページングを検討する。
## タイプ
| コード | 表示名 | 特徴 |
| --- | --- | --- |
| SPARK | スパーク | 新しいことに素早く動く |
| HARBOR | ハーバー | 場の安心感を作る |
| ROOT | ルート | 土台を整えて進める |
| CRAFT | クラフト | 品質や細部を高める |
| BLOOM | ブルーム | 人の良さを見つけ広げる |
## 相性ルール
| 自分 | 相性のよいタイプ |
| --- | --- |
| SPARK | HARBOR, ROOT |
| HARBOR | SPARK, CRAFT |
| ROOT | SPARK, BLOOM |
| CRAFT | HARBOR, BLOOM |
| BLOOM | ROOT, CRAFT |
## テスト方針
- 単体テスト: タイプ判定、相性候補抽出
- 結合テスト: POST /diagnosesで保存後に相性候補が返ること
- システムテスト: 30人分の名前で登録して画面表示が崩れないこと
# 要件定義メモ
## 目的
新人研修のチーム開発として、AWSの最小構成を使ったWebアプリケーションをV字開発工程に沿って作成する。対象者は社内同期30人程度とし、診断後に同期同士の相性を表示する。
## スコープ
### 含める
- 名前入力
- 10問の二択診断
- 5種類のオリジナルタイプ判定
- 診断結果の保存
- 診断者本人と相性のよいタイプの登録者表示
### 含めない
- ログイン
- 管理画面
- 診断結果の編集・削除
- 複雑なランキング
- メール通知
- MBTIという名称や既存16タイプの使用
## 機能要件
| ID | 要件 | 優先度 |
| --- | --- | --- |
| F-01 | 利用者は名前を入力できる | 必須 |
| F-02 | 利用者は診断質問に回答できる | 必須 |
| F-03 | システムは回答から5種類のタイプを判定できる | 必須 |
| F-04 | システムは名前、タイプ、診断日時を保存できる | 必須 |
| F-05 | システムは相性のよいタイプの登録者名を表示できる | 必須 |
## 非機能要件
| ID | 要件 | 方針 |
| --- | --- | --- |
| NF-01 | 利用規模 | 同期30人程度を想定 |
| NF-02 | 可用性 | 研修発表時に動作することを重視 |
| NF-03 | セキュリティ | 個人情報は名前のみ、公開範囲は研修内に限定 |
| NF-04 | 保守性 | 未経験者が読める単純な構成にする |
## V字開発での成果物例
| 工程 | 成果物 |
| --- | --- |
| 要件定義 | 本書、画面イメージ、機能一覧 |
| 基本設計 | 画面設計、API設計、DB設計 |
| 詳細設計 | 判定ロジック、バリデーション、エラー処理 |
| 実装 | フロント、Lambda、SAMテンプレート |
| 単体テスト | 判定ロジック、相性抽出 |
| 結合テスト | フロントからAPI経由で保存・表示 |
| システムテスト | 30人利用想定の手動確認 |
Readme
# Sync Type Finder
社内同期向けの、MBTI風オリジナル診断Webアプリケーションです。新人研修の11日間開発を想定し、AWSの利用要素を Lambda / DynamoDB / API Gateway に絞った最小構成にしています。
## 機能
- 名前を入力して10問の診断に回答する
- 5種類のオリジナルタイプから診断結果を表示する
- 診断結果をDynamoDBに保存する
- 保存済みの診断者から、相性のよいタイプの人を表示する
## AWS構成
- API Gateway: HTTP APIとしてフロントからLambdaを呼び出す
- Lambda: 診断結果の保存と相性候補の取得
- DynamoDB: 診断者の名前、タイプ、作成日時を保存
## ディレクトリ
```text
.
├── docs/
│ ├── requirements.md
│ └── basic-design.md
├── frontend/
│ ├── index.html
│ ├── styles.css
│ └── app.js
├── src/
│ └── diagnosis.mjs
├── tests/
│ └── diagnosis.test.mjs
├── template.yaml
└── server.cjs
```
## ローカルで画面確認
```powershell
node server.cjs 4173
```
ブラウザで `http://127.0.0.1:4173/index.html` を開きます。
ローカル表示ではAPI未設定のため、ブラウザ内のモック保存で結果画面まで確認できます。AWSデプロイ後は `frontend/app.js` の `API_BASE_URL` にAPI GatewayのURLを設定してください。
## テスト
```powershell
node --test
```
## デプロイの目安
AWS SAM CLIが使える環境で以下を実行します。
```powershell
sam build
sam deploy --guided
```
デプロイ後に出力される `ApiUrl` を `frontend/app.js` の `API_BASE_URL` に設定し、静的ファイルを任意の配布先に置きます。今回の課題条件ではAWS最小要素を Lambda / DynamoDB / API Gateway に限定するため、S3/CloudFrontは必須にしていません。
設計書
- 要件定義書
- 1. 文書情報
- 2. 背景
- 3. 目的
- 4. システム概要
- 5. スコープ
- 6. 利用者
- 7. 業務フロー
- 8. 画面イメージ
- 9. 機能一覧
- 10. 機能要件
- 11. 非機能要件
- 12. 制約条件
- 13. 受入基準
- 14. V字開発での成果物
- 基本設計書
- 1. 文書情報
- 2. システム構成
- 3. コンポーネント責務
- 4. 画面設計
- 5. API設計
- 6. DB設計
- 7. タイプ設計
- 8. AWS設計
- 9. 画面/API連携
- 10. テスト設計方針
- 11. 実装担当分担案
- 詳細設計
- 1. 文書情報
- 2. 詳細設計の対象範囲
- 3. 定数設計
- 4. 質問設計
- 5. 診断判定ロジック
- 6. 相性候補抽出ロジック
- 7. バリデーション設計
- 8. API処理詳細
- 9. DynamoDBアクセス設計
- 10. エラー処理設計
- 11. セキュリティ詳細
- 12. テストケース設計
- 13. 実装上の注意点
- 14. 将来改善案
- 1. この資料の目的
- 2. 今回作るシステムを一言でいうと
- 3. このシステムでできること
- 4. あえて作らないもの
- 5. 全体構成
- 6. 利用者が診断したときの流れ
- 7. なぜブラウザだけで完結させないのか
- 8. なぜAPI Gatewayを使うのか
- 9. なぜLambdaを使うのか
- 10. なぜDynamoDBを使うのか
- 11. ファイル構成
- 12. 各ファイルの役割
- 13. 診断ロジックの仕組み
- 14. 診断ロジックの例
- 15. 同点になったらどうするか
- 16. 相性表示の仕組み
- 17. なぜ自分自身を除外するのか
- 18. 入力チェックの仕組み
- 19. エラー処理の仕組み
- 20. ローカル確認モードの仕組み
- 21. AWSデプロイ時の仕組み
- 22. テストの考え方
- 23. V字開発との対応
- 24. この設計で大事な考え方
- 25. 初学者が理解すべきポイント
- 26. よくある疑問
- 27. 発表で説明するときの流れ
- 28. このシステムのまとめ
要件定義書
要件定義書
1. 文書情報
| 項目 | 内容 |
|---|---|
| 文書名 | Sync Type Finder 要件定義書 |
| 対象システム | 社内同期向けオリジナルタイプ診断Webアプリケーション |
| 想定工程 | 新人研修におけるV字開発工程 |
| 開発期間 | 11日間 |
| 開発体制 | 5名、全員未経験者を想定 |
| 想定利用者数 | 最低30名程度 |
| 使用AWSサービス | Lambda、DynamoDB、API Gateway |
2. 背景
新人研修のチーム開発課題として、AWSを最低限利用したWebアプリケーションを作成する。
題材はMBTI診断サイトに近い体験とするが、既存のMBTI名称、16タイプ、設問体系は使用せず、研修用のオリジナルタイプ診断として実装する。
診断後、利用者の名前と診断結果を保存し、保存済みの同期の中から相性のよいタイプの人を表示する。
利用者は社内同期を想定し、実利用規模は30人程度とする。
3. 目的
本システムの目的は以下である。
| ID | 目的 |
|---|---|
| P-01 | 利用者が短時間でオリジナルタイプ診断を完了できること |
| P-02 | 診断結果と名前を保存し、同期同士の相性表示に利用できること |
| P-03 | Lambda、DynamoDB、API Gatewayを使った基本的なWebアプリ構成を学習できること |
| P-04 | V字開発工程に沿って、要件定義、設計、実装、テストを一通り経験できること |
4. システム概要
利用者はWeb画面から名前を入力し、10問の二択質問に回答する。
システムは回答内容を集計し、5種類のオリジナルタイプのうち最も点数が高いタイプを診断結果として表示する。
診断結果はAPI Gateway経由でLambdaに送信され、DynamoDBへ保存される。
保存済みの診断結果から、利用者のタイプと相性がよいタイプの人を抽出し、結果画面に表示する。
5. スコープ
5.1 スコープ内
| ID | 内容 |
|---|---|
| IN-01 | 名前入力 |
| IN-02 | 10問の二択診断 |
| IN-03 | 5種類のオリジナルタイプ判定 |
| IN-04 | 診断結果画面の表示 |
| IN-05 | 診断者名、診断タイプ、診断日時の保存 |
| IN-06 | 保存済み診断者から相性のよい人を表示 |
| IN-07 | 診断ロジックの単体テスト |
| IN-08 | API経由で保存、取得できることの結合確認 |
5.2 スコープ外
| ID | 内容 | 理由 |
|---|---|---|
| OUT-01 | ログイン、認証 | 11日間、未経験5名では設計と実装負荷が高い |
| OUT-02 | 管理画面 | 必須価値に直結しない |
| OUT-03 | 診断結果の編集、削除 | 利用規模が小さく、初期要件としては不要 |
| OUT-04 | ランキング機能 | 仕様が広がりやすいため除外 |
| OUT-05 | メール、Slack通知 | AWS外部連携が増え、研修範囲を超えやすい |
| OUT-06 | S3、CloudFrontによる静的ホスティング | AWS最低限要素をLambda、DynamoDB、API Gatewayに絞る |
| OUT-07 | 既存MBTI名称、16タイプの使用 | 商標、内容の誤解、設問設計の複雑化を避ける |
6. 利用者
| 利用者 | 説明 | 主な操作 |
|---|---|---|
| 社内同期 | 研修参加者または同期社員 | 名前入力、診断回答、結果確認 |
| 開発メンバー | 新人研修の開発担当者 | 実装、テスト、デプロイ、発表 |
| 研修担当者 | 成果物を確認する担当者 | 要件、設計、動作確認、レビュー |
7. 業務フロー
flowchart TD
A["利用者が診断画面を開く"] --> B["名前を入力する"]
B --> C["10問の質問に回答する"]
C --> D["診断ボタンを押す"]
D --> E["システムがタイプを判定する"]
E --> F["診断結果をDynamoDBに保存する"]
F --> G["相性のよい登録者を検索する"]
G --> H["結果と相性のよい人を表示する"]
8. 画面イメージ
8.1 診断画面
┌──────────────────────────────────────────────┐
│ 同期タイプ診断 回答数 0 / 10 │
│ Sync Type Finder │
├──────────────────────────────────────────────┤
│ 名前 │
│ [ 佐藤 ] │
│ │
│ 1. 新しい課題が始まったとき、最初にやりたいことは? │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ まず動くものを作る │ │ 目的と前提を整理する │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ 2. チームで話し合うときに意識しやすいことは? │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 意見を出して流れを作る │ │ 全員が話しやすい状態にする │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ [ 診断する ] │
└──────────────────────────────────────────────┘
8.2 結果画面
┌──────────────────────────────┐
│ 診断結果 │
│ 佐藤さんはスパークタイプ │
│ │
│ 新しいことに素早く反応し、 │
│ 場を前に進めるタイプです。 │
│ │
│ 相性のよい同期 │
│ あなたは田中さん、鈴木さんと │
│ 相性がいい! │
│ │
│ [ もう一度診断する ] │
└──────────────────────────────┘
9. 機能一覧
| ID | 機能名 | 概要 | 優先度 |
|---|---|---|---|
| F-01 | 名前入力 | 利用者が診断者名を入力する | 必須 |
| F-02 | 質問表示 | 10問の二択質問を表示する | 必須 |
| F-03 | 回答入力 | 各質問に対して1つの選択肢を選ぶ | 必須 |
| F-04 | 回答数表示 | 回答済み数を表示する | 任意 |
| F-05 | 診断判定 | 回答スコアから5タイプのいずれかを判定する | 必須 |
| F-06 | 診断結果表示 | 名前、タイプ、説明文を表示する | 必須 |
| F-07 | 診断結果保存 | 名前、タイプ、診断日時をDynamoDBへ保存する | 必須 |
| F-08 | 相性候補抽出 | 保存済み診断者から相性のよい人を抽出する | 必須 |
| F-09 | 相性メッセージ表示 | 「あなたは○○さんと相性がいい!」を表示する | 必須 |
| F-10 | 再診断 | 画面を初期状態に戻して再診断できる | 任意 |
10. 機能要件
F-01 名前入力
| 項目 | 内容 |
|---|---|
| 概要 | 利用者が自身の名前を入力できる |
| 入力 | 名前 |
| 入力制約 | 1文字以上30文字以下 |
| 必須 | 必須 |
| 正常時 | 入力した名前を診断結果保存時に使用する |
| 異常時 | 未入力の場合、診断を開始しない |
| 受入条件 | 名前未入力で診断ボタンを押した場合、送信されないこと |
F-02 質問表示
| 項目 | 内容 |
|---|---|
| 概要 | 診断用の質問を10問表示する |
| 表示項目 | 質問文、選択肢2件 |
| 受入条件 | 10問すべてが画面に表示されること |
F-03 回答入力
| 項目 | 内容 |
|---|---|
| 概要 | 各質問に対して二択のうち1つを選択できる |
| 入力方式 | ラジオボタン |
| 受入条件 | 各質問で同時に選択できる選択肢は1つであること |
F-04 回答数表示
| 項目 | 内容 |
|---|---|
| 概要 | 回答済みの質問数を表示する |
| 表示形式 | 回答済み数 / 質問数 |
| 受入条件 | 選択状態を変更したとき、回答済み数が更新されること |
F-05 診断判定
| 項目 | 内容 |
|---|---|
| 概要 | 回答ごとのスコアを合計し、最も点数の高いタイプを判定する |
| タイプ数 | 5種類 |
| タイプ | SPARK、HARBOR、ROOT、CRAFT、BLOOM |
| 同点時 | 定義済みの優先順で1つに決定する |
| 受入条件 | 全回答から1つの診断タイプが返ること |
F-06 診断結果表示
| 項目 | 内容 |
|---|---|
| 概要 | 診断者名、診断タイプ、タイプ説明を表示する |
| 表示例 | 佐藤さんはスパークタイプ |
| 受入条件 | 診断完了後に結果画面が表示されること |
F-07 診断結果保存
| 項目 | 内容 |
|---|---|
| 概要 | 診断結果をDynamoDBに保存する |
| 保存項目 | id、name、type、createdAt |
| 保存タイミング | 診断ボタン押下後、判定完了時 |
| 受入条件 | 診断完了後、DynamoDBに1件保存されること |
F-08 相性候補抽出
| 項目 | 内容 |
|---|---|
| 概要 | 自分のタイプと相性のよいタイプの登録者を抽出する |
| 除外条件 | 自分自身と同一名のデータは除外する |
| 最大表示件数 | 5件 |
| 受入条件 | 相性対象タイプの人だけが返ること |
F-09 相性メッセージ表示
| 項目 | 内容 |
|---|---|
| 概要 | 相性のよい人の名前を結果画面に表示する |
| 表示例 | あなたは田中さんと相性がいい! |
| 候補なし | まだ相性のよい同期が登録されていません。 を表示する |
| 受入条件 | 候補あり、候補なしの両方で適切な文言が表示されること |
F-10 再診断
| 項目 | 内容 |
|---|---|
| 概要 | 診断フォームを初期状態に戻す |
| 受入条件 | ボタン押下後、入力内容と回答がクリアされること |
11. 非機能要件
| ID | 分類 | 要件 | 指標、方針 |
|---|---|---|---|
| NF-01 | 性能 | 30人程度の利用で問題なく動作する | API応答は通常3秒以内を目安とする |
| NF-02 | 可用性 | 研修発表時に動作確認できる | 発表前にデプロイ済みURLで疎通確認する |
| NF-03 | セキュリティ | 保存する個人情報を最小化する | 保存項目は名前と診断タイプに限定する |
| NF-04 | 保守性 | 未経験者でも追える構造にする | ファイル数、機能数を増やしすぎない |
| NF-05 | 拡張性 | 将来タイプ数や質問数を変更できる | 質問、タイプ、相性ルールを定数として管理する |
| NF-06 | 操作性 | スマートフォンでも回答できる | 幅860px以下で1カラム表示にする |
| NF-07 | コスト | 研修用途で低コストに運用する | DynamoDBはオンデマンド課金、Lambdaは128MB |
12. 制約条件
| ID | 制約 | 内容 |
|---|---|---|
| C-01 | 開発期間 | 11日間で要件定義からテストまで完了させる |
| C-02 | スキル | 開発者は全員未経験者を想定する |
| C-03 | AWSサービス | 最低限Lambda、DynamoDB、API Gatewayを使用する |
| C-04 | データ量 | 利用者30人程度を前提にする |
| C-05 | 法務、表現 | MBTIという名称や既存タイプ体系は画面上で使用しない |
13. 受入基準
| ID | 確認内容 | 合格条件 |
|---|---|---|
| A-01 | 診断画面表示 | 名前入力、10問、診断ボタンが表示される |
| A-02 | 未入力制御 | 名前未入力または未回答がある場合、診断できない |
| A-03 | タイプ判定 | 全回答後、5タイプのうち1つが表示される |
| A-04 | データ保存 | 診断後、DynamoDBに診断者データが保存される |
| A-05 | 相性表示 | 相性のよいタイプの登録者名が表示される |
| A-06 | 候補なし表示 | 相性候補がいない場合、候補なしメッセージが表示される |
| A-07 | スマホ表示 | スマホ幅でも質問と結果が読める |
| A-08 | テスト | 判定ロジックと相性抽出の単体テストが成功する |
14. V字開発での成果物
| 工程 | 成果物 | 本プロジェクトでの対応 |
|---|---|---|
| 要件定義 | 本書、画面イメージ、機能一覧 | docs/requirements.md |
| 基本設計 | 画面設計、API設計、DB設計 | docs/basic-design.md |
| 詳細設計 | 判定ロジック、バリデーション、エラー処理 | docs/detailed-design.md |
| 実装 | ソースコード、SAMテンプレート | frontend/, src/, template.yaml |
| 単体テスト | テストコード、テスト結果 | tests/diagnosis.test.mjs |
| 結合テスト | API疎通確認、画面操作確認 | 手動テストで確認 |
| システムテスト | 受入基準に基づく確認 | 受入基準表で確認 |
基本設計書
基本設計書
1. 文書情報
| 項目 | 内容 |
|---|---|
| 文書名 | Sync Type Finder 基本設計書 |
| 対象システム | 社内同期向けオリジナルタイプ診断Webアプリケーション |
| 対応要件 | docs/requirements.md |
| 主な設計対象 | 画面、API、DynamoDB、AWS構成 |
2. システム構成
flowchart LR
U["利用者ブラウザ"] --> F["静的フロントエンド<br>HTML/CSS/JavaScript"]
F -->|POST /diagnoses| A["API Gateway<br>HTTP API"]
F -->|GET /diagnoses| A
A --> L["Lambda<br>diagnosis.handler"]
L --> D["DynamoDB<br>DiagnosisTable"]
3. コンポーネント責務
| コンポーネント | ファイル、AWSリソース | 責務 |
|---|---|---|
| フロントエンド | frontend/index.html | 画面のHTML構造を定義する |
| フロントエンド | frontend/styles.css | レイアウト、配色、レスポンシブ表示を定義する |
| フロントエンド | frontend/app.js | 質問表示、入力制御、API呼び出し、結果表示を行う |
| API | API Gateway HTTP API | ブラウザからのHTTPリクエストをLambdaへ転送する |
| バックエンド | src/diagnosis.mjs | 診断判定、保存、相性候補抽出を行う |
| DB | DynamoDB DiagnosisTable | 診断者名、診断タイプ、診断日時を保存する |
| IaC | template.yaml | AWSリソースをSAMで定義する |
4. 画面設計
4.1 画面一覧
| 画面ID | 画面名 | URL | 概要 |
|---|---|---|---|
| SCR-01 | 診断画面 | /index.html | 名前入力と診断質問への回答を行う |
| SCR-02 | 結果画面 | /index.html | 診断結果と相性のよい人を表示する |
本アプリでは画面遷移を増やさず、1つのHTML内で診断画面と結果画面を表示する。
4.2 SCR-01 診断画面
4.2.1 目的
利用者が名前を入力し、10問の二択質問に回答して診断を開始する。
4.2.2 レイアウト
┌──────────────────────────────────────────────┐
│ 同期タイプ診断 回答数 0 / 10 │
│ Sync Type Finder │
├──────────────────────────────────────────────┤
│ 名前 │
│ [ name input ] │
│ │
│ 質問1 │
│ [選択肢A] [選択肢B] │
│ │
│ 質問2 │
│ [選択肢A] [選択肢B] │
│ │
│ ... │
│ │
│ [診断する] │
│ メッセージ表示欄 │
└──────────────────────────────────────────────┘
4.2.3 画面項目
| 項目ID | 項目名 | 種別 | 必須 | 入力、表示仕様 |
|---|---|---|---|---|
| SCR01-I01 | 名前 | テキスト入力 | 必須 | 1文字以上30文字以下 |
| SCR01-I02 | 質問 | テキスト表示 | – | 10問表示 |
| SCR01-I03 | 選択肢 | ラジオボタン | 必須 | 各質問につき2択、1つのみ選択可 |
| SCR01-O01 | 回答数 | テキスト表示 | – | 回答済み数 / 10 |
| SCR01-B01 | 診断する | ボタン | – | 押下時に入力チェック後、診断処理を実行 |
| SCR01-M01 | メッセージ | テキスト表示 | – | 入力不備、保存失敗時のメッセージを表示 |
4.2.4 画面イベント
| イベントID | 操作 | 処理 |
|---|---|---|
| SCR01-E01 | 名前入力 | 入力値をフォームに保持する |
| SCR01-E02 | 選択肢選択 | 回答済み数を更新する |
| SCR01-E03 | 診断する押下 | 入力チェック、回答収集、API呼び出し、結果表示を行う |
4.3 SCR-02 結果画面
4.3.1 目的
診断結果と、相性のよい同期を利用者に表示する。
4.3.2 レイアウト
┌──────────────────────────────┐
│ 診断結果 │
│ {name}さんは{label}タイプ │
│ │
│ {type description} │
│ │
│ 相性のよい同期 │
│ あなたは{name}さんと相性がいい! │
│ │
│ [もう一度診断する] │
└──────────────────────────────┘
4.3.3 画面項目
| 項目ID | 項目名 | 種別 | 表示仕様 |
|---|---|---|---|
| SCR02-O01 | 診断結果タイトル | テキスト表示 | {診断者名}さんは{タイプ表示名}タイプ |
| SCR02-O02 | タイプ説明 | テキスト表示 | タイプごとの説明文 |
| SCR02-O03 | 相性候補見出し | テキスト表示 | 相性のよい同期 |
| SCR02-O04 | 相性メッセージ | テキスト表示 | 候補あり、候補なしで文言を切り替える |
| SCR02-B01 | もう一度診断する | ボタン | フォーム初期化、結果画面非表示 |
4.3.4 表示パターン
| パターン | 条件 | 表示 |
|---|---|---|
| P-01 | 相性候補が1件 | あなたは田中さんと相性がいい! |
| P-02 | 相性候補が複数件 | あなたは田中さん、鈴木さんと相性がいい! |
| P-03 | 相性候補が0件 | まだ相性のよい同期が登録されていません。次の人の診断を待ちましょう。 |
5. API設計
5.1 API一覧
| API ID | メソッド | パス | 概要 | 利用画面 |
|---|---|---|---|---|
| API-01 | POST | /diagnoses | 診断結果を保存し、相性候補を返す | 診断画面 |
| API-02 | GET | /diagnoses | 保存済み診断者を取得する | 動作確認、結合テスト |
5.2 共通仕様
| 項目 | 内容 |
|---|---|
| データ形式 | JSON |
| 文字コード | UTF-8 |
| CORS | 全オリジン許可。研修用途のため最小設定 |
| 認証 | なし |
| エラー形式 | { "message": "エラー内容" } |
5.3 API-01 POST /diagnoses
5.3.1 概要
診断回答を受け取り、タイプを判定する。
判定結果をDynamoDBへ保存し、保存済みデータから相性候補を抽出して返す。
5.3.2 リクエスト
POST /diagnoses
Content-Type: application/json
{
"name": "佐藤",
"answers": [
{ "SPARK": 2, "CRAFT": 1 },
{ "SPARK": 2, "BLOOM": 1 },
{ "ROOT": 2, "CRAFT": 1 }
]
}
5.3.3 リクエスト項目
| 項目 | 型 | 必須 | 制約 |
|---|---|---|---|
| name | string | 必須 | trim後1文字以上30文字以下 |
| answers | array | 必須 | 1件以上。通常は10件 |
| answers[].SPARK | number | 任意 | 整数 |
| answers[].HARBOR | number | 任意 | 整数 |
| answers[].ROOT | number | 任意 | 整数 |
| answers[].CRAFT | number | 任意 | 整数 |
| answers[].BLOOM | number | 任意 | 整数 |
5.3.4 レスポンス
ステータス: 201 Created
{
"id": "a1b2c3d4-0000-0000-0000-000000000000",
"name": "佐藤",
"result": {
"type": "SPARK",
"label": "スパーク",
"compatibleTypes": ["HARBOR", "ROOT"],
"scores": {
"SPARK": 5,
"HARBOR": 0,
"ROOT": 2,
"CRAFT": 2,
"BLOOM": 1
}
},
"matches": [
{
"name": "田中",
"type": "HARBOR",
"label": "ハーバー"
}
]
}
5.3.5 レスポンス項目
| 項目 | 型 | 内容 |
|---|---|---|
| id | string | 保存した診断結果のUUID |
| name | string | 診断者名 |
| result.type | string | 診断タイプコード |
| result.label | string | 診断タイプ表示名 |
| result.compatibleTypes | array | 相性対象タイプコード |
| result.scores | object | タイプ別スコア |
| matches | array | 相性候補 |
| matches[].name | string | 相性候補者名 |
| matches[].type | string | 相性候補者タイプ |
| matches[].label | string | 相性候補者タイプ表示名 |
5.4 API-02 GET /diagnoses
5.4.1 概要
保存済み診断者を取得する。
主に動作確認、結合テスト、研修発表時の確認に使用する。
5.4.2 リクエスト
GET /diagnoses
タイプで絞り込む場合:
GET /diagnoses?type=SPARK
5.4.3 クエリパラメータ
| 項目 | 型 | 必須 | 内容 |
|---|---|---|---|
| type | string | 任意 | 指定したタイプの診断者のみ取得する |
5.4.4 レスポンス
ステータス: 200 OK
{
"items": [
{
"name": "佐藤",
"type": "SPARK",
"label": "スパーク",
"createdAt": "2026-06-05T10:00:00.000Z"
}
]
}
6. DB設計
6.1 テーブル一覧
| テーブル論理名 | テーブル物理名 | 用途 |
|---|---|---|
| 診断結果テーブル | DiagnosisTable | 診断者の名前、タイプ、診断日時を保存する |
6.2 DiagnosisTable
| 属性名 | 型 | キー | 必須 | 説明 |
|---|---|---|---|---|
| id | String | パーティションキー | 必須 | UUID |
| name | String | – | 必須 | 診断者名 |
| type | String | – | 必須 | 診断タイプコード |
| createdAt | String | – | 必須 | ISO8601形式の登録日時 |
6.3 データ例
{
"id": "a1b2c3d4-0000-0000-0000-000000000000",
"name": "佐藤",
"type": "SPARK",
"createdAt": "2026-06-05T10:00:00.000Z"
}
6.4 キー設計方針
| 項目 | 方針 |
|---|---|
| パーティションキー | id |
| GSI | 作成しない |
| 読み取り方式 | 件数が30人程度のためScanを利用する |
| 重複登録 | 同一人物が複数回診断しても別レコードとして保存する |
GSIを作成しない理由は、研修範囲と利用規模を踏まえ、設計と実装を単純化するためである。
実サービス化する場合は、type をパーティションキーにしたGSI、または最新診断のみ保持する設計を検討する。
7. タイプ設計
7.1 タイプ一覧
| タイプコード | 表示名 | 説明 |
|---|---|---|
| SPARK | スパーク | 新しいことに素早く反応し、場を前に進める |
| HARBOR | ハーバー | 周囲をよく見て、安心して話せる空気を作る |
| ROOT | ルート | 物事の土台を整え、着実に形にしていく |
| CRAFT | クラフト | 細部を観察し、品質や完成度を高める |
| BLOOM | ブルーム | 人の良さや可能性を見つけ、関係を広げる |
7.2 相性ルール
| 自分のタイプ | 相性のよいタイプ |
|---|---|
| SPARK | HARBOR, ROOT |
| HARBOR | SPARK, CRAFT |
| ROOT | SPARK, BLOOM |
| CRAFT | HARBOR, BLOOM |
| BLOOM | ROOT, CRAFT |
8. AWS設計
8.1 API Gateway
| 項目 | 設計値 |
|---|---|
| 種別 | HTTP API |
| CORS | GET, POST, OPTIONS を許可 |
| 認証 | なし |
| 統合先 | Lambda DiagnosisFunction |
8.2 Lambda
| 項目 | 設計値 |
|---|---|
| Runtime | Node.js 20.x |
| Handler | diagnosis.handler |
| Timeout | 10秒 |
| Memory | 128MB |
| 環境変数 | TABLE_NAME |
| IAM | 対象DynamoDBテーブルへのCRUD権限 |
8.3 DynamoDB
| 項目 | 設計値 |
|---|---|
| BillingMode | PAY_PER_REQUEST |
| 主キー | id |
| TTL | 使用しない |
| Stream | 使用しない |
9. 画面/API連携
sequenceDiagram
participant User as 利用者
participant Front as フロントエンド
participant Api as API Gateway
participant Lambda as Lambda
participant DB as DynamoDB
User->>Front: 名前入力、質問回答
User->>Front: 診断する押下
Front->>Front: 入力チェック、回答収集
Front->>Api: POST /diagnoses
Api->>Lambda: リクエスト転送
Lambda->>Lambda: タイプ判定
Lambda->>DB: PutItem
Lambda->>DB: Scan
Lambda->>Lambda: 相性候補抽出
Lambda->>Api: 結果JSON
Api->>Front: 結果JSON
Front->>User: 診断結果、相性候補表示
10. テスト設計方針
| テスト種別 | 対象 | 確認観点 |
|---|---|---|
| 単体テスト | calculateType | スコア合計、最大スコアタイプ判定、同点時の動作 |
| 単体テスト | findMatches | 相性対象タイプ抽出、自分自身除外、最大5件 |
| 結合テスト | POST /diagnoses | リクエスト送信、保存、レスポンス形式 |
| 結合テスト | GET /diagnoses | 保存済みデータ取得、タイプ絞り込み |
| システムテスト | 画面操作 | 名前入力、10問回答、結果表示、スマホ表示 |
11. 実装担当分担案
| 担当 | 主担当領域 | 成果物 |
|---|---|---|
| メンバーA | 要件、画面仕様 | 要件定義書、画面項目 |
| メンバーB | フロントHTML/CSS | index.html, styles.css |
| メンバーC | フロントJavaScript | app.js |
| メンバーD | Lambda/API | diagnosis.mjs, API疎通 |
| メンバーE | AWS/SAM/テスト | template.yaml, テスト、デプロイ |
詳細設計
詳細設計書
1. 文書情報
| 項目 | 内容 |
|---|---|
| 文書名 | Sync Type Finder 詳細設計書 |
| 対象システム | 社内同期向けオリジナルタイプ診断Webアプリケーション |
| 対応要件 | docs/requirements.md |
| 対応基本設計 | docs/basic-design.md |
| 主な設計対象 | 判定ロジック、バリデーション、エラー処理 |
2. 詳細設計の対象範囲
| 対象 | 内容 |
|---|---|
| 診断判定 | 回答スコアから5種類のタイプを決定する |
| 相性候補抽出 | 保存済み診断者から相性のよい人を抽出する |
| 入力チェック | フロントエンド、バックエンドで入力値を検証する |
| エラー処理 | API、画面で異常時の扱いを定義する |
| テストケース | 単体テスト、結合テスト、画面テストの観点を定義する |
3. 定数設計
3.1 タイプコード
| コード | 表示名 | 優先順位 | 備考 |
|---|---|---|---|
| SPARK | スパーク | 1 | 同点時に最も優先 |
| HARBOR | ハーバー | 2 | |
| ROOT | ルート | 3 | |
| CRAFT | クラフト | 4 | |
| BLOOM | ブルーム | 5 | 同点時に最も後 |
同点時は、上記の優先順位に従って1つのタイプを決定する。
これは診断結果を必ず1つにするための仕様である。
3.2 タイプ表示名
const TYPE_LABELS = {
SPARK: "スパーク",
HARBOR: "ハーバー",
ROOT: "ルート",
CRAFT: "クラフト",
BLOOM: "ブルーム",
};
3.3 相性ルール
const TYPE_COMPATIBILITY = {
SPARK: ["HARBOR", "ROOT"],
HARBOR: ["SPARK", "CRAFT"],
ROOT: ["SPARK", "BLOOM"],
CRAFT: ["HARBOR", "BLOOM"],
BLOOM: ["ROOT", "CRAFT"],
};
4. 質問設計
4.1 質問数
| 項目 | 設計値 |
|---|---|
| 質問数 | 10問 |
| 選択肢数 | 各問2択 |
| 回答方式 | ラジオボタン |
| スコア方式 | 選択肢ごとに複数タイプへ点数を加算 |
4.2 回答データ構造
フロントエンドからバックエンドへ送信する回答は、タイプコードと点数のオブジェクト配列とする。
[
{ "SPARK": 2, "CRAFT": 1 },
{ "HARBOR": 2, "BLOOM": 1 }
]
4.3 スコア付与ルール
| 項目 | 内容 |
|---|---|
| 主タイプ | 選択肢の主な傾向に対して2点 |
| 補助タイプ | 関連する傾向に対して1点 |
| 未指定タイプ | 0点 |
例:
{ "SPARK": 2, "CRAFT": 1 }
この場合、SPARKに2点、CRAFTに1点を加算する。
5. 診断判定ロジック
5.1 対象関数
| 関数名 | ファイル | 役割 |
|---|---|---|
calculateType | src/diagnosis.mjs | 回答配列から診断タイプを決定する |
5.2 入力
answers: Array<Record<string, number>>
例:
[
{ "SPARK": 2, "CRAFT": 1 },
{ "SPARK": 2, "BLOOM": 1 },
{ "ROOT": 2, "SPARK": 1 }
]
5.3 出力
{
"type": "SPARK",
"label": "スパーク",
"compatibleTypes": ["HARBOR", "ROOT"],
"scores": {
"SPARK": 5,
"HARBOR": 0,
"ROOT": 2,
"CRAFT": 1,
"BLOOM": 1
}
}
5.4 処理手順
| 手順 | 内容 |
|---|---|
| 1 | answers が空でない配列であることを確認する |
| 2 | 5タイプすべての初期スコアを0にする |
| 3 | 各回答オブジェクトを先頭から順に処理する |
| 4 | 回答内のタイプコードが定義済みタイプであることを確認する |
| 5 | 回答内の点数が整数であることを確認する |
| 6 | 対象タイプのスコアに点数を加算する |
| 7 | スコアが高い順にタイプを並べ替える |
| 8 | 同点の場合はタイプ優先順位で並べ替える |
| 9 | 先頭のタイプを診断結果とする |
| 10 | タイプ、表示名、相性タイプ、スコアを返す |
5.5 疑似コード
function calculateType(answers):
if answers is not array or answers length is 0:
throw validation error
scores = {
SPARK: 0,
HARBOR: 0,
ROOT: 0,
CRAFT: 0,
BLOOM: 0
}
for each answer in answers:
if answer is not object:
throw validation error
for each type, value in answer:
if type is not defined:
throw validation error
if value is not integer:
throw validation error
scores[type] = scores[type] + value
sortedTypes = sort scores by:
1. score desc
2. type priority asc
resultType = sortedTypes[0]
return {
type: resultType,
label: TYPE_LABELS[resultType],
compatibleTypes: TYPE_COMPATIBILITY[resultType],
scores
}
5.6 同点時の例
入力:
[
{ "SPARK": 2 },
{ "HARBOR": 2 }
]
スコア:
| タイプ | 点数 |
|---|---|
| SPARK | 2 |
| HARBOR | 2 |
| ROOT | 0 |
| CRAFT | 0 |
| BLOOM | 0 |
結果:
{
"type": "SPARK"
}
理由: SPARKとHARBORが同点の場合、優先順位が高いSPARKを採用する。
6. 相性候補抽出ロジック
6.1 対象関数
| 関数名 | ファイル | 役割 |
|---|---|---|
findMatches | src/diagnosis.mjs | 保存済み診断者から相性のよい人を抽出する |
6.2 入力
records: Array<DiagnosisRecord>
ownName: string
ownType: string
DiagnosisRecord:
{
"name": "田中",
"type": "HARBOR",
"createdAt": "2026-06-05T10:00:00.000Z"
}
6.3 出力
[
{
"name": "田中",
"type": "HARBOR",
"label": "ハーバー"
}
]
6.4 処理手順
| 手順 | 内容 |
|---|---|
| 1 | 自分のタイプに対応する相性タイプ配列を取得する |
| 2 | 保存済み診断者のうち、タイプが相性タイプに含まれるものだけを抽出する |
| 3 | 診断者名をtrim、小文字化して、自分自身と同一名のデータを除外する |
| 4 | createdAt の昇順で並べる |
| 5 | 最大5件までに制限する |
| 6 | 表示に必要な name, type, label のみに整形して返す |
6.5 自分自身の除外仕様
| 比較対象 | 変換 | 例 |
|---|---|---|
| 自分の名前 | trim().toLowerCase() | " Sato " → "sato" |
| 保存済みの名前 | trim().toLowerCase() | "sato" → "sato" |
変換後に一致する場合は自分自身とみなし、候補から除外する。
日本語の表記ゆれ、漢字違い、旧字体までは判定しない。
7. バリデーション設計
7.1 フロントエンドのバリデーション
| 項目 | ルール | 実装方法 | エラー時 |
|---|---|---|---|
| 名前 | 必須 | HTML required | ブラウザ標準の入力エラー |
| 名前 | 最大30文字 | HTML maxlength="30" | 30文字を超えて入力できない |
| 回答 | 各質問必須 | radio required | ブラウザ標準の入力エラー |
| 回答数 | 10問すべて回答 | JSで回答数確認 | メッセージ欄に表示 |
7.2 バックエンドのバリデーション
| 対象 | ルール | エラー内容 |
|---|---|---|
| request body | JSONとして解析できる | 解析不可の場合400 |
| name | trim後1文字以上 | 400 |
| name | trim後30文字以下 | 400 |
| answers | 配列である | 400 |
| answers | 1件以上 | 400 |
| answer | オブジェクトである | 400 |
| type | 定義済みタイプである | 400 |
| score | 整数である | 400 |
7.3 バリデーションエラーのレスポンス
{
"message": "name must be 1 to 30 characters"
}
ステータスコードは 400 Bad Request とする。
7.4 バリデーション方針
| 方針 | 理由 |
|---|---|
| フロントとバックエンドの両方でチェックする | 画面以外からAPIを呼ばれる可能性があるため |
| エラー文言は短くする | 研修用途のため、実装と確認を単純化する |
| 名前以外の個人情報は入力させない | 保存データを最小限にするため |
8. API処理詳細
8.1 POST /diagnoses
8.1.1 正常処理
| 手順 | 処理 |
|---|---|
| 1 | リクエストボディをJSONとして解析する |
| 2 | name を検証し、前後空白を除去する |
| 3 | answers を calculateType に渡して診断結果を取得する |
| 4 | UUIDを生成する |
| 5 | 現在日時をISO8601文字列で生成する |
| 6 | id, name, type, createdAt をDynamoDBに保存する |
| 7 | DynamoDBから全診断結果をScanで取得する |
| 8 | findMatches で相性候補を抽出する |
| 9 | ステータス201で結果JSONを返す |
8.1.2 保存データ
{
"id": "uuid",
"name": "佐藤",
"type": "SPARK",
"createdAt": "2026-06-05T10:00:00.000Z"
}
8.1.3 注意事項
現在の実装では、同じ名前で複数回診断した場合も別レコードとして保存する。
これは、ログインなしで本人性を厳密に判断できないためである。
将来、最新結果のみを保持したい場合は、名前をキーにする設計または認証機能が必要になる。
8.2 GET /diagnoses
8.2.1 正常処理
| 手順 | 処理 |
|---|---|
| 1 | DynamoDBから全診断結果をScanで取得する |
| 2 | type クエリパラメータがある場合、そのタイプのみ抽出する |
| 3 | 表示用に name, type, label, createdAt へ整形する |
| 4 | ステータス200で結果JSONを返す |
8.2.2 注意事項
type に未定義タイプが指定された場合でも、現状は空配列を返す。
研修用途では十分だが、厳密にする場合は未定義タイプを400エラーにする。
9. DynamoDBアクセス設計
9.1 PutItem
| 項目 | 内容 |
|---|---|
| 使用タイミング | POST /diagnoses |
| 目的 | 診断結果を1件保存する |
| 保存先 | TABLE_NAME 環境変数で指定されたテーブル |
保存時のAttributeValue:
{
"id": { "S": "uuid" },
"name": { "S": "佐藤" },
"type": { "S": "SPARK" },
"createdAt": { "S": "2026-06-05T10:00:00.000Z" }
}
9.2 Scan
| 項目 | 内容 |
|---|---|
| 使用タイミング | POST /diagnoses、GET /diagnoses |
| 目的 | 保存済み診断者を取得する |
| 件数想定 | 30件程度 |
30件程度のためScanを採用する。
大量データを扱う場合は、Scanは非効率なため設計変更が必要である。
10. エラー処理設計
10.1 ステータスコード
| ステータス | 発生条件 | レスポンス例 |
|---|---|---|
| 200 | GET /diagnoses 正常終了 | { "items": [] } |
| 201 | POST /diagnoses 正常終了 | { "id": "...", "result": {...} } |
| 400 | リクエスト形式不正、入力不備 | { "message": "name must be 1 to 30 characters" } |
| 405 | 未対応メソッド | { "message": "Method not allowed" } |
| 500 | DynamoDB障害、環境変数不足など | { "message": "Internal server error" } |
10.2 フロントエンドのエラー表示
| 発生箇所 | 条件 | 表示 |
|---|---|---|
| 入力チェック | 未回答がある | すべての質問に回答してください。 |
| API呼び出し | HTTPステータスが2xx以外 | 診断結果を保存できませんでした。 |
| API呼び出し | 通信失敗 | ブラウザの例外メッセージまたは保存失敗メッセージ |
10.3 Lambdaのエラー処理
| エラー種別 | 処理 |
|---|---|
| JSON解析エラー | 400を返す |
| バリデーションエラー | 400を返す |
| 未対応HTTPメソッド | 405を返す |
| DynamoDBエラー | CloudWatch Logsへ出力し、500を返す |
TABLE_NAME 未設定 | CloudWatch Logsへ出力し、500を返す |
10.4 ログ出力方針
| 項目 | 方針 |
|---|---|
| 出力先 | Lambda標準ログ、CloudWatch Logs |
| 出力内容 | エラーオブジェクト |
| 個人情報 | 必要以上に名前をログへ出さない |
11. セキュリティ詳細
| 観点 | 設計 |
|---|---|
| 認証 | 研修用途のためなし |
| 保存データ | 名前、タイプ、日時のみ |
| CORS | 研修用途では全許可。本番用途では配布元ドメインに限定する |
| XSS | 画面生成時に質問文、選択肢をエスケープする |
| 権限 | Lambdaには対象DynamoDBテーブルのCRUD権限のみ付与する |
12. テストケース設計
12.1 単体テスト
| TC ID | 対象 | 条件 | 期待結果 |
|---|---|---|---|
| UT-01 | calculateType | SPARKが最高点 | SPARKが返る |
| UT-02 | calculateType | HARBORが最高点 | HARBORが返る |
| UT-03 | calculateType | 同点 | 優先順位が高いタイプが返る |
| UT-04 | calculateType | answersが空配列 | エラーになる |
| UT-05 | calculateType | 未定義タイプを含む | エラーになる |
| UT-06 | calculateType | scoreが小数 | エラーになる |
| UT-07 | findMatches | 相性タイプが存在する | 候補が返る |
| UT-08 | findMatches | 自分と同名のレコードがある | 候補から除外される |
| UT-09 | findMatches | 候補が6件以上 | 最大5件になる |
| UT-10 | findMatches | 候補なし | 空配列が返る |
12.2 結合テスト
| TC ID | 対象 | 条件 | 期待結果 |
|---|---|---|---|
| IT-01 | POST /diagnoses | 正常なname、answers | 201が返り、DynamoDBに保存される |
| IT-02 | POST /diagnoses | name未指定 | 400が返る |
| IT-03 | POST /diagnoses | answers未指定 | 400が返る |
| IT-04 | GET /diagnoses | データ登録済み | itemsに保存済みデータが返る |
| IT-05 | GET /diagnoses?type=SPARK | SPARK登録済み | SPARKのみ返る |
12.3 画面テスト
| TC ID | 対象 | 条件 | 期待結果 |
|---|---|---|---|
| ST-01 | 診断画面 | 初期表示 | 名前入力、10問、診断ボタンが表示される |
| ST-02 | 診断画面 | 回答選択 | 回答数が更新される |
| ST-03 | 診断画面 | 未回答あり | 診断できない |
| ST-04 | 結果画面 | 相性候補あり | 候補者名が表示される |
| ST-05 | 結果画面 | 相性候補なし | 候補なし文言が表示される |
| ST-06 | レスポンシブ | スマホ幅 | 1カラムで表示され、文字がはみ出さない |
13. 実装上の注意点
| 項目 | 注意点 |
|---|---|
| 質問変更 | frontend/app.js の questions を変更する |
| タイプ変更 | フロントとバックエンドのタイプ定義を同時に変更する |
| 相性変更 | フロントのローカル確認用定義とバックエンド定義を合わせる |
| API接続 | AWSデプロイ後、API_BASE_URL にAPI Gateway URLを設定する |
| 外部ライブラリ | 研修負荷を下げるため追加しない |
14. 将来改善案
本開発ではスコープ外とするが、実サービス化する場合は以下を検討する。
| 改善案 | 内容 |
|---|---|
| 認証 | 社内アカウントでログインし、本人の最新診断のみ保存する |
| 静的ホスティング | S3、CloudFrontでフロントを配信する |
| DB最適化 | type で検索できるGSIを追加する |
| 管理画面 | 登録者一覧、削除、集計を行う |
| 入力保護 | CORSを特定ドメインに限定する |
| 監視 | CloudWatch AlarmでLambdaエラーを検知する |
実際の仕事っぽく進めるなら、方針はかなりはっきりしています。
「木村さんが全部作る」ではなく、「木村さんを実装加速役・技術リードにして、他4人が仕様・設計・テスト・AWS確認を握る」 のが一番いいです。
5人全員未経験寄りで11日しかないなら、全員が全部を均等にやるより、V字工程の成果物に責任者を置いたほうが現実的です。
基本方針
- 要件は増やさない
- 画面は1画面構成にする
- APIは
POST /diagnosesとGET /diagnosesだけにする - DBは1テーブルだけにする
- 認証、管理画面、削除、ランキング、集計は入れない
- 木村さんは実装を進めるが、仕様決定者にはしすぎない
- 他メンバーは「設計書・テスト・レビュー・発表」で価値を出す
この課題で一番危ないのは、AIを使える人が爆速でコードを書いて、他4人が「何ができているのか分からない」状態になることです。実務だとこれはかなり危険です。納期には間に合っても、レビュー、説明、障害対応、テストが弱くなります。
おすすめの役割分担
| 役割 | 担当 | 主な責任 |
|---|---|---|
| PM/要件担当 | 1人 | スコープ管理、日程管理、要件定義書 |
| 画面/UX担当 | 1人 | 画面設計、HTML/CSS、画面テスト |
| API/DB担当 | 1人 | API設計、DB設計、DynamoDB確認 |
| テスト/品質担当 | 1人 | テスト仕様書、単体/結合/受入テスト |
| 技術リード | 木村さん | 実装支援、AI活用、詰まり解消、コードレビュー |
木村さんは「全部実装する人」ではなく、「全員が進められるようにする人」にしたほうがいいです。実装が必要なところでは強く使う。ただし、仕様や設計の説明責任は各担当が持つ、という形が仕事っぽいです。
11日間の進め方
| 日 | 工程 | やること |
|---|---|---|
| 1日目 | 要件定義 | 作る/作らないを決める、画面イメージ、機能一覧 |
| 2日目 | 要件定義 | 受入基準、非機能要件、スコープ確定 |
| 3日目 | 基本設計 | 画面設計、API設計、DB設計、AWS構成 |
| 4日目 | 詳細設計 | 判定ロジック、バリデーション、エラー処理、テスト観点 |
| 5日目 | 実装 | フロント骨組み、Lambda骨組み、SAMテンプレート |
| 6日目 | 実装 | 診断ロジック、API保存、結果表示 |
| 7日目 | 実装/単体テスト | 単体テスト、画面調整、バグ修正 |
| 8日目 | 結合テスト | API Gateway/Lambda/DynamoDB連携確認 |
| 9日目 | システムテスト | 30人分想定データ、スマホ表示、発表環境確認 |
| 10日目 | 修正/資料 | バグ修正、設計書との整合、発表資料 |
| 11日目 | 発表 | デモ、工程説明、振り返り |
大事なのは、4日目までに仕様を固定する ことです。
5日目以降に「タイプを増やそう」「管理画面も欲しい」「ランキング欲しい」となったら、実務なら変更要求として扱います。研修では基本的に却下でいいです。
実際の仕事っぽい進行方法
毎日、短い朝会をやるといいです。
話す内容はこれだけで十分です。
- 昨日やったこと
- 今日やること
- 困っていること
- 仕様変更になりそうなこと
特に4つ目が大事です。新人チームは「ちょっと便利そうだから追加」が起きやすいです。でもV字工程では、後半の仕様変更は設計書、実装、テスト全部に影響します。
レビューのやり方
レビューはコードだけ見ないほうがいいです。
見る順番はこれがいいです。
- 要件定義書と実装が合っているか
- 基本設計書とAPI/DBが合っているか
- 詳細設計書と判定ロジックが合っているか
- テストで受入基準を確認できるか
- 最後にコードが読めるか
実務では「コードが動く」より「決めた仕様どおりに動く」が大事です。
AIの使い方
木村さんのAI活用は強みなので、使ったほうがいいです。ただし使い方を決めておくべきです。
おすすめはこうです。
- AIに実装案を出させる
- AIに設計書のたたき台を出させる
- AIにテストケースを洗い出させる
- AIにコードレビュー観点を出させる
- ただし、採用判断は人間がする
- AI生成コードは木村さん以外が説明できる状態にする
発表で「木村がAIで作りました」になると弱いです。
「AIを使って実装速度を上げたが、要件・設計・テストはチームでレビューし、全員が説明できるようにした」と言えると、かなり仕事っぽいです。
このチームで一番大事な成功条件
一番大事なのは、完成度の高い巨大アプリではなく、要件、設計、実装、テストが一本の線でつながっている小さいアプリ を作ることです。
今回なら成功形はこれです。
- 要件定義書に「名前、診断、保存、相性表示」と書いてある
- 基本設計書に画面、API、DBが書いてある
- 詳細設計書に判定ロジック、バリデーション、エラー処理が書いてある
- 実装がその通りになっている
- テストでそれを確認している
- 発表で5人全員が自分の担当を説明できる
これなら、機能が少なくても実務的にはかなり良い成果物です。
逆に、機能が多くても設計書と実装がズレていたら、仕事としては評価されにくいです。
Sync Type Finder システム全体解説資料
1. この資料の目的
この資料は、今回作成した診断サイトの仕組みを初学者にも分かるように説明するための資料です。
単に「このファイルは何をしているか」だけでなく、以下も説明します。
- このシステムが何をするものなのか
- 利用者が操作したとき、裏側で何が起きるのか
- なぜAPI Gateway、Lambda、DynamoDBを使うのか
- なぜ機能を増やしすぎない設計にしているのか
- 各ファイルがどの役割を持っているのか
- V字開発でどの成果物と対応しているのか
2. 今回作るシステムを一言でいうと
社内同期向けのオリジナルタイプ診断サイトです。
利用者は名前を入力し、10問の質問に回答します。
システムは回答内容から5種類のオリジナルタイプのうち1つを判定します。
診断後、名前と診断結果を保存し、すでに診断した同期の中から相性のよい人を表示します。
表示例:
佐藤さんはスパークタイプ
あなたは田中さん、鈴木さんと相性がいい!
3. このシステムでできること
| 機能 | 内容 |
|---|---|
| 名前入力 | 診断する人の名前を入力する |
| 診断回答 | 10問の二択質問に回答する |
| タイプ判定 | 回答スコアから5種類のタイプのうち1つを決める |
| 結果表示 | 診断タイプと説明文を表示する |
| 結果保存 | 名前、タイプ、診断日時をDynamoDBに保存する |
| 相性表示 | 保存済みの人から相性のよい人を表示する |
4. あえて作らないもの
今回の開発では、以下は作りません。
| 作らないもの | 理由 |
|---|---|
| ログイン機能 | 11日間、未経験5人の研修では重すぎるため |
| 管理画面 | 診断サイトの最低限の価値に直結しないため |
| 削除、編集機能 | データ量が30人程度で、初期要件として不要なため |
| ランキング機能 | 仕様が広がりやすく、設計とテストが増えるため |
| メール、Slack通知 | 外部連携が増えて難易度が上がるため |
| 既存MBTIの16タイプ | 既存名称や体系を使わず、研修用のオリジナル診断にするため |
これは手抜きではありません。
実際の仕事でも、期間、人数、技術力、目的に合わせて「作らないもの」を決めることはとても重要です。
今回の目的は、大きなアプリを作ることではなく、要件定義、設計、実装、テストがつながった小さなシステムを完成させることです。
5. 全体構成
今回のシステムは、以下のような構成です。
flowchart LR
User["利用者"] --> Browser["ブラウザ<br>HTML / CSS / JavaScript"]
Browser --> Api["API Gateway"]
Api --> Lambda["Lambda"]
Lambda --> Dynamo["DynamoDB"]
それぞれの役割は以下です。
| 部品 | 役割 |
|---|---|
| ブラウザ | 画面を表示し、利用者の入力を受け取る |
| HTML | 画面の構造を作る |
| CSS | 画面の見た目を整える |
| JavaScript | 質問表示、回答収集、API呼び出し、結果表示を行う |
| API Gateway | ブラウザからのHTTPリクエストをLambdaに渡す入口 |
| Lambda | 診断判定、保存、相性候補抽出を行う処理本体 |
| DynamoDB | 診断結果を保存するデータベース |
6. 利用者が診断したときの流れ
利用者が診断ボタンを押すと、以下の順番で処理が進みます。
sequenceDiagram
participant User as 利用者
participant Front as ブラウザ画面
participant Api as API Gateway
participant Lambda as Lambda
participant DB as DynamoDB
User->>Front: 名前を入力し、10問に回答する
User->>Front: 診断ボタンを押す
Front->>Front: 入力内容をチェックする
Front->>Front: 回答をJSON形式にまとめる
Front->>Api: POST /diagnoses を送る
Api->>Lambda: リクエストを転送する
Lambda->>Lambda: 回答からタイプを判定する
Lambda->>DB: 名前、タイプ、日時を保存する
Lambda->>DB: 保存済みの診断結果を取得する
Lambda->>Lambda: 相性のよい人を探す
Lambda->>Api: 診断結果と相性候補を返す
Api->>Front: レスポンスを返す
Front->>User: 結果画面を表示する
7. なぜブラウザだけで完結させないのか
診断タイプを表示するだけなら、ブラウザだけでも作れます。
実際、ローカル確認用にはブラウザ内の localStorage に仮保存する処理も入っています。
しかし、ブラウザだけで完結すると問題があります。
| 問題 | 説明 |
|---|---|
| 他の人の診断結果が見えない | 自分のブラウザにしかデータが保存されない |
| 相性表示が成立しにくい | 同期全員の結果を共通の場所に保存できない |
| AWSを使ったWebアプリ学習になりにくい | サーバー側処理、DB保存、API連携を経験できない |
そのため、今回の本番想定ではDynamoDBに全員の診断結果を保存します。
8. なぜAPI Gatewayを使うのか
API Gatewayは、ブラウザからLambdaを呼び出すための入口です。
ブラウザは直接Lambda関数を実行できません。
そこで、API GatewayがURLを用意します。
例:
https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/diagnoses
ブラウザはこのURLに対してHTTPリクエストを送ります。
API GatewayはそのリクエストをLambdaに渡します。
今回のAPIは2つだけです。
| メソッド | パス | 役割 |
|---|---|---|
| POST | /diagnoses | 診断結果を保存し、相性候補を返す |
| GET | /diagnoses | 保存済み診断者を取得する |
9. なぜLambdaを使うのか
Lambdaは、サーバーを自分で用意しなくても処理を実行できるAWSサービスです。
通常のWebアプリでは、バックエンド用のサーバーを起動し続けることがあります。
しかし、今回のような小規模な診断サイトでは、常にサーバーを動かし続ける必要はありません。
Lambdaを使うと、リクエストが来たときだけ処理が動きます。
Lambdaで行う処理は以下です。
| 処理 | 内容 |
|---|---|
| 入力チェック | 名前や回答が正しい形式か確認する |
| タイプ判定 | 回答スコアから診断タイプを決める |
| 保存 | DynamoDBに診断結果を保存する |
| 取得 | DynamoDBから保存済みデータを取得する |
| 相性候補抽出 | 自分と相性のよいタイプの人を探す |
| レスポンス作成 | ブラウザに返すJSONを作る |
10. なぜDynamoDBを使うのか
DynamoDBはAWSのデータベースサービスです。
今回のシステムでは、診断結果を保存するために使います。
保存するデータはかなりシンプルです。
| 項目 | 例 | 説明 |
|---|---|---|
| id | uuid | 1件の診断結果を識別するID |
| name | 佐藤 | 診断した人の名前 |
| type | SPARK | 診断タイプ |
| createdAt | 2026-06-05T10:00:00.000Z | 診断日時 |
今回の利用者は30人程度です。
そのため、複雑な検索設計はせず、DynamoDBのテーブルを1つだけ使います。
実務ではデータ量が多い場合、検索用のインデックスを作ることがあります。
しかし今回は研修用途なので、まずは「保存できる」「取得できる」「相性表示に使える」を優先します。
11. ファイル構成
.
├── docs/
│ ├── requirements.md
│ ├── basic-design.md
│ ├── detailed-design.md
│ └── system-explanation.md
├── frontend/
│ ├── index.html
│ ├── styles.css
│ └── app.js
├── src/
│ └── diagnosis.mjs
├── tests/
│ └── diagnosis.test.mjs
├── template.yaml
├── server.cjs
├── package.json
└── README.md
12. 各ファイルの役割
12.1 frontend/index.html
画面の骨組みを作るファイルです。
HTMLは、画面に何を置くかを決めます。
このファイルには、以下のような部品があります。
- タイトル
- 名前入力欄
- 質問を表示する場所
- 診断ボタン
- 結果を表示する場所
- 再診断ボタン
HTMLだけでは、質問を自動表示したり、診断ボタンを押したときに処理を動かしたりはできません。
その動きは frontend/app.js が担当します。
12.2 frontend/styles.css
画面の見た目を整えるファイルです。
CSSは、画面をどう見せるかを決めます。
このファイルでは、以下を定義しています。
- 画面全体の幅
- 2カラム、1カラムのレイアウト
- 色
- ボタンの見た目
- 質問カードの見た目
- スマートフォン表示
CSSを分けておくことで、HTMLの構造と見た目の調整を別々に管理できます。
12.3 frontend/app.js
ブラウザ側の動きを担当するファイルです。
主な処理は以下です。
| 処理 | 内容 |
|---|---|
| 質問定義 | 10問の質問文、選択肢、スコアを持つ |
| 質問表示 | HTMLに質問を描画する |
| 回答数更新 | 何問回答したかを表示する |
| 回答収集 | 選択された回答を配列にまとめる |
| API呼び出し | POST /diagnoses に回答を送る |
| ローカル確認 | API未設定時はlocalStorageで仮保存する |
| 結果表示 | 診断タイプと相性候補を画面に出す |
重要なのは、API_BASE_URL です。
const API_BASE_URL = "";
ここが空の場合、ローカル確認モードとして動きます。
AWSデプロイ後は、API GatewayのURLをここに入れます。
例:
const API_BASE_URL = "https://xxxxx.execute-api.ap-northeast-1.amazonaws.com";
12.4 src/diagnosis.mjs
Lambdaで動くバックエンド処理です。
このファイルが、システムの中核です。
主な関数は以下です。
| 関数 | 役割 |
|---|---|
calculateType | 回答から診断タイプを判定する |
findMatches | 保存済みデータから相性のよい人を探す |
handler | API Gatewayから呼ばれる入口 |
handleCreate | POST /diagnoses の処理 |
handleList | GET /diagnoses の処理 |
sanitizeName | 名前の入力チェック |
putItem | DynamoDBへ保存 |
scanRecords | DynamoDBから取得 |
ブラウザ側にも診断ロジックがありますが、それはローカル確認用です。
本番で信用するべき判定と保存は、Lambda側で行います。
13. 診断ロジックの仕組み
今回の診断は、5つのタイプに点数を加算していく方式です。
タイプは以下です。
| タイプコード | 表示名 | 特徴 |
|---|---|---|
| SPARK | スパーク | 新しいことに素早く動く |
| HARBOR | ハーバー | 場の安心感を作る |
| ROOT | ルート | 土台を整えて進める |
| CRAFT | クラフト | 品質や細部を高める |
| BLOOM | ブルーム | 人の良さを見つけ広げる |
各選択肢にはスコアが設定されています。
例:
{ SPARK: 2, CRAFT: 1 }
これは、以下の意味です。
SPARKに2点
CRAFTに1点
10問すべてのスコアを合計し、一番点数が高いタイプを診断結果にします。
14. 診断ロジックの例
利用者が以下のように回答したとします。
[
{ "SPARK": 2, "CRAFT": 1 },
{ "SPARK": 2, "BLOOM": 1 },
{ "ROOT": 2, "SPARK": 1 }
]
合計するとこうなります。
| タイプ | 点数 |
|---|---|
| SPARK | 5 |
| HARBOR | 0 |
| ROOT | 2 |
| CRAFT | 1 |
| BLOOM | 1 |
一番点数が高いのはSPARKなので、結果はスパークタイプになります。
15. 同点になったらどうするか
点数が同じになる可能性があります。
例:
| タイプ | 点数 |
|---|---|
| SPARK | 4 |
| HARBOR | 4 |
| ROOT | 1 |
| CRAFT | 1 |
| BLOOM | 0 |
この場合、必ず1つの結果を出すために、優先順位を決めています。
優先順位:
SPARK → HARBOR → ROOT → CRAFT → BLOOM
この例では、SPARKとHARBORが同点ですが、SPARKのほうが優先順位が高いのでSPARKになります。
同点ルールを決めておかないと、同じ回答でも結果が不安定になる可能性があります。
実務では、このような細かいルールを設計書に書いておくことが重要です。
16. 相性表示の仕組み
相性は、タイプ同士の対応表で決めています。
| 自分のタイプ | 相性のよいタイプ |
|---|---|
| SPARK | HARBOR, ROOT |
| HARBOR | SPARK, CRAFT |
| ROOT | SPARK, BLOOM |
| CRAFT | HARBOR, BLOOM |
| BLOOM | ROOT, CRAFT |
たとえば、自分がSPARKの場合、HARBORまたはROOTの人が相性候補になります。
処理の流れは以下です。
1. 自分の診断タイプを確認する
2. 相性のよいタイプ一覧を取得する
3. DynamoDBに保存されている診断者一覧を取得する
4. 相性のよいタイプの人だけを残す
5. 自分と同じ名前の人を除外する
6. 最大5人まで返す
17. なぜ自分自身を除外するのか
診断後、自分の結果もDynamoDBに保存されます。
そのまま相性候補を探すと、自分自身が候補に含まれる可能性があります。
それを避けるため、同じ名前のデータは相性候補から除外しています。
ただし、ログイン機能がないため、完全に本人を判定できるわけではありません。
例:
佐藤さんが2人いる場合、同じ名前として扱われる可能性がある
今回の研修用途では、そこまで厳密にしない設計にしています。
実サービスであれば、社員IDやログイン機能を使うべきです。
18. 入力チェックの仕組み
入力チェックは、フロントエンドとバックエンドの両方で行います。
18.1 フロントエンド側
ブラウザ側では、以下をチェックします。
| 項目 | チェック |
|---|---|
| 名前 | 必須 |
| 名前 | 最大30文字 |
| 質問 | 各質問で1つ選択必須 |
| 回答数 | 10問すべて回答しているか |
フロントエンド側でチェックすると、利用者にすぐエラーを伝えられます。
18.2 バックエンド側
Lambda側でもチェックします。
| 項目 | チェック |
|---|---|
| name | 1文字以上30文字以下 |
| answers | 空でない配列 |
| answer | オブジェクト |
| type | 定義済みタイプ |
| score | 整数 |
バックエンドでもチェックする理由は、画面を通さずにAPIを直接呼ぶこともできるからです。
フロントだけのチェックでは、システム全体としては不十分です。
19. エラー処理の仕組み
APIでは、正常時と異常時でステータスコードを分けます。
| ステータス | 意味 | 例 |
|---|---|---|
| 200 | 正常に取得できた | GET /diagnoses |
| 201 | 正常に作成、保存できた | POST /diagnoses |
| 400 | 入力内容が不正 | 名前が空、回答形式が不正 |
| 405 | 対応していないHTTPメソッド | PUTやDELETEなど |
| 500 | サーバー側の問題 | DynamoDB障害、環境変数不足 |
エラー時は以下のようなJSONを返します。
{
"message": "name must be 1 to 30 characters"
}
画面側では、保存に失敗した場合に以下のようなメッセージを表示します。
診断結果を保存できませんでした。
20. ローカル確認モードの仕組み
frontend/app.js の API_BASE_URL が空の場合、API Gatewayには接続しません。
代わりに、ブラウザの localStorage に診断結果を保存します。
API_BASE_URL が空
↓
AWSに送らない
↓
ブラウザ内に仮保存
↓
結果画面まで確認できる
これにより、AWSへデプロイする前でも画面の動きや診断結果表示を確認できます。
ただし、localStorage はそのブラウザだけの保存場所です。
他の人のブラウザとは共有されません。
本番想定では、DynamoDBに保存する必要があります。
21. AWSデプロイ時の仕組み
AWSへデプロイするときは、template.yaml を使います。
template.yaml は、AWSリソースをコードとして定義するファイルです。
このような考え方を Infrastructure as Code、略してIaCと呼びます。
今回定義している主なリソースは以下です。
| リソース | 内容 |
|---|---|
| DiagnosisTable | DynamoDBテーブル |
| DiagnosisApi | API Gateway HTTP API |
| DiagnosisFunction | Lambda関数 |
SAM CLIを使うと、このテンプレートをもとにAWS上へリソースを作成できます。
sam build
sam deploy --guided
デプロイ後、API GatewayのURLが出力されます。
そのURLを frontend/app.js の API_BASE_URL に設定すると、画面からAWSのAPIを呼べるようになります。
22. テストの考え方
テストでは、ただ画面を触るだけではなく、ロジック単位でも確認します。
今回用意しているテストファイルは以下です。
tests/diagnosis.test.mjs
確認していることは主に2つです。
| テスト | 内容 |
|---|---|
| タイプ判定テスト | 回答スコアから正しいタイプが返るか |
| 相性抽出テスト | 自分を除外し、相性のよい人だけ返るか |
テストを実行するコマンド:
node --test
実務では、画面テスト、APIテスト、DB保存確認も行います。
今回の研修では、最低限以下を確認できればよいです。
| 確認 | 内容 |
|---|---|
| 単体テスト | 診断ロジック、相性抽出 |
| 結合テスト | API Gateway、Lambda、DynamoDBがつながる |
| システムテスト | 画面から診断して結果が表示される |
23. V字開発との対応
V字開発では、左側で要件と設計を決め、右側でそれをテストします。
flowchart TD
Req["要件定義"] --> Basic["基本設計"]
Basic --> Detail["詳細設計"]
Detail --> Impl["実装"]
Impl --> Unit["単体テスト"]
Unit --> Integration["結合テスト"]
Integration --> System["システムテスト"]
今回の成果物との対応は以下です。
| 工程 | 成果物 | 内容 |
|---|---|---|
| 要件定義 | docs/requirements.md | 何を作るか、何を作らないか |
| 基本設計 | docs/basic-design.md | 画面、API、DB、AWS構成 |
| 詳細設計 | docs/detailed-design.md | 判定ロジック、入力チェック、エラー処理 |
| 実装 | frontend/, src/, template.yaml | 実際に動くコード |
| 単体テスト | tests/diagnosis.test.mjs | ロジックの確認 |
| 結合テスト | API疎通確認 | APIとDBがつながるか |
| システムテスト | 画面操作確認 | 利用者目線で動くか |
24. この設計で大事な考え方
24.1 小さく作る
今回のシステムは、あえて小さく作っています。
理由は、11日間、5人、全員未経験という条件では、機能を増やしすぎると設計、実装、テストが破綻しやすいからです。
小さく作ることで、以下ができます。
- 要件を説明しやすい
- 設計書と実装を一致させやすい
- テストしやすい
- 発表で全体像を説明しやすい
24.2 役割を分ける
HTML、CSS、JavaScript、Lambda、DynamoDB、SAMテンプレートは、それぞれ役割が違います。
役割を分けると、修正するときにどこを見ればよいか分かりやすくなります。
例:
| やりたいこと | 見るファイル |
|---|---|
| 画面の構造を変えたい | frontend/index.html |
| 色や余白を変えたい | frontend/styles.css |
| 質問文を変えたい | frontend/app.js |
| 診断判定を変えたい | src/diagnosis.mjs |
| AWS構成を変えたい | template.yaml |
24.3 画面だけを信用しない
フロントエンドで入力チェックをしていても、バックエンドでも入力チェックをします。
これは実務でとても重要です。
画面は利用者にとって便利にするための場所であり、バックエンドはシステムを守るための場所です。
24.4 設計書と実装を対応させる
今回の資料では、要件、基本設計、詳細設計、実装がつながるようにしています。
たとえば:
要件: 診断結果を保存する
↓
基本設計: POST /diagnoses と DiagnosisTable を設計する
↓
詳細設計: handleCreate と PutItem の処理手順を書く
↓
実装: src/diagnosis.mjs にコードを書く
↓
テスト: API実行後にDynamoDBへ保存されたか確認する
このつながりがあると、実務のレビューでも説明しやすくなります。
25. 初学者が理解すべきポイント
このシステムを理解するうえで、最初に押さえるべきポイントは以下です。
| ポイント | 説明 |
|---|---|
| フロントエンド | 利用者が見る画面 |
| バックエンド | 画面の裏側で動く処理 |
| API | フロントエンドとバックエンドのやり取りの入口 |
| データベース | データを保存する場所 |
| JSON | フロントとバックエンドがやり取りするデータ形式 |
| HTTPメソッド | POSTは作成、GETは取得 |
| バリデーション | 入力内容が正しいか確認すること |
| レスポンス | APIが返す結果 |
| ステータスコード | API処理の成功、失敗を表す番号 |
26. よくある疑問
Q1. なぜ診断結果を保存するのか
相性表示をするためです。
保存していないと、他の人の診断結果を使って相性候補を出せません。
Q2. なぜDynamoDBは1テーブルだけなのか
保存するデータが少なく、利用者も30人程度だからです。
複雑なテーブル設計にすると、研修期間内で理解しづらくなります。
Q3. なぜScanを使っているのか
データ件数が少ないからです。
30人程度なら全件取得してから絞り込んでも問題になりにくいです。
ただし、実サービスで何千人、何万人が使うならScanは避け、検索用インデックスを設計します。
Q4. なぜログインがないのか
ログインを作ると、認証、権限、ユーザー管理、セキュリティ設計が必要になります。
今回の研修目的では、そこまで広げるより、Webアプリの基本構成を完成させるほうが重要です。
Q5. なぜフロントとLambdaの両方に診断ロジックがあるのか
フロント側の診断ロジックは、ローカル確認用です。
API未設定でも画面の動きを確認できるようにしています。
本番ではLambda側の診断ロジックを正とします。
将来的には、フロントのローカル確認用ロジックを削除し、常にAPI経由にしてもよいです。
27. 発表で説明するときの流れ
発表では、以下の順番で説明すると分かりやすいです。
- 何を作ったか
- なぜこの題材にしたか
- 利用者の操作の流れ
- AWS構成
- 画面設計
- API設計
- DB設計
- 診断ロジック
- テスト
- 工夫した点
- 苦労した点
- 今後の改善案
特に大事なのは、「機能が多い」ことではなく、「なぜこの設計にしたか」を説明することです。
28. このシステムのまとめ
この診断サイトは、以下の流れで動きます。
利用者が回答する
↓
ブラウザが回答を集める
↓
API Gatewayに送る
↓
Lambdaが診断タイプを判定する
↓
DynamoDBに保存する
↓
保存済みデータから相性候補を探す
↓
ブラウザに結果を返す
↓
画面に「あなたは○○さんと相性がいい!」と表示する
今回のシステムは小さいですが、Webアプリケーションの基本要素が含まれています。
- 画面
- API
- サーバー側処理
- データベース
- 入力チェック
- エラー処理
- テスト
- AWS構成
- 設計書
そのため、新人研修のV字開発課題として、全体像を学ぶにはちょうどよい規模のシステムです。


コメント