メインコンテンツまでスキップ

Survey用フロントパッケージ

MBC CQRS Serverlessアプリケーションでアンケートテンプレートの管理とフォームレンダリングを行うためのフロントエンドコンポーネントライブラリです。

インストール

npm install @mbc-cqrs-serverless/survey-web
バージョン情報

v0.0.42で、ReactとReact DOMがpeer dependenciesとして外部化され、Context分離問題が解消されました。v0.0.41以前をお使いの場合は、v0.0.42以降にアップグレードしてください。

概要

Survey Webパッケージ(@mbc-cqrs-serverless/survey-web)は、アンケートテンプレートの作成、編集、レンダリング用のReactコンポーネントを提供します。複数の質問タイプ、ドラッグ&ドロップによるセクション並び替え、AWS AppSyncを通じたリアルタイムコラボレーションをサポートしています。

機能

  • テンプレート管理: アンケートテンプレートの作成、編集、削除
  • 複数の質問タイプ: 様々なデータ収集ニーズに対応する9種類の組み込み質問タイプ
  • ドラッグ&ドロップ: @dnd-kitによるセクションと質問の並び替え
  • フォームバリデーション: カスタムルール対応のZodベースバリデーション
  • リアルタイム更新: AWS AppSync統合による共同編集
  • レスポンシブデザイン: モバイルフレンドリーなアンケートフォーム
  • セクションベースの構造: 質問を論理的なセクションに整理

主要コンポーネント

SurveyTemplatePage

検索・管理機能を備えたアンケートテンプレート一覧を表示します。

import { SurveyTemplatePage } from "@mbc-cqrs-serverless/survey-web/SurveyTemplatePage";
import "@mbc-cqrs-serverless/survey-web/styles.css";

export default function SurveyTemplatesPage() {
return <SurveyTemplatePage />;
}

EditSurveyTemplatePage

ドラッグ&ドロップ機能を備えたアンケートテンプレートの作成・編集エディタ。

このコンポーネントは内部的にnext/navigationuseParams()を使用してURLからアンケートIDを取得します。新規作成モードでは、IDパラメータのないルートでレンダリングします。既存のアンケートを編集する場合は、IDパラメータ付きのルート(例:/surveys/[id]/edit)でレンダリングします。

import { EditSurveyTemplatePage } from "@mbc-cqrs-serverless/survey-web/EditSurveyTemplatePage";

// Route: /surveys/new (create mode) (新規作成モード)
// Route: /surveys/[id]/edit (edit mode - ID extracted from URL via useParams) (編集モード - useParamsでURLからIDを取得)
export default function EditSurveyPage() {
return <EditSurveyTemplatePage />;
}

SurveyForm

アンケートテンプレートを回答者向けの入力フォームとしてレンダリングします。

import { SurveyForm } from "@mbc-cqrs-serverless/survey-web/SurveyForm";

// Answer values can be string (single value) or string[] (multiple choice) (回答値は文字列(単一値)または文字列配列(複数選択))
type SurveyAnswers = Record<string, string | string[] | undefined>;

// Define schema type based on the Survey Template Structure section below (以下のアンケートテンプレート構造セクションに基づいてスキーマ型を定義)
interface SurveySchema {
title: string;
description?: string;
items: SurveyItem[];
}

interface Props {
schema: SurveySchema;
}

export default function SurveyResponsePage({ schema }: Props) {
const handleSubmit = (answers: SurveyAnswers) => {
console.log("Survey answers:", answers);
};

return (
<SurveyForm
schema={schema}
onSubmit={handleSubmit}
disabled={false}
>
{/* Optional: Custom content rendered inside the current section (オプション: 現在のセクション内にレンダリングされるカスタムコンテンツ) */}
</SurveyForm>
);
}
プロパティ必須説明
schemaSurveySchemaはいレンダリングするアンケートテンプレートスキーマ
onSubmit(answers: SurveyAnswers) => voidはいすべての回答でアンケートが送信されたときのコールバック
disabledbooleanいいえフォーム操作を無効化(デフォルト:false)
childrenReact.ReactNodeいいえ現在のセクション内にレンダリングされるオプションのコンテンツ

質問タイプ

Survey Webパッケージは9種類の質問タイプをサポートしています:

1. 短いテキスト

簡潔な回答用の1行テキスト入力。

{
"id": "q1",
"type": "short-text",
"label": "What is your name?",
"validation": {
"required": true
}
}

2. 長いテキスト

詳細な回答用の複数行テキストエリア。

{
"id": "q2",
"type": "long-text",
"label": "Please describe your experience",
"validation": {
"required": false,
"custom": {
"type": "length",
"rule": "max",
"value": 1000,
"customError": "Response must be 1000 characters or less"
}
}
}

3. 単一選択

相互排他的な選択肢用のラジオボタン。

{
"id": "q3",
"type": "single-choice",
"label": "What is your preferred contact method?",
"options": [
{ "value": "email", "label": "Email" },
{ "value": "phone", "label": "Phone" },
{ "value": "mail", "label": "Mail" }
],
"validation": {
"required": true
}
}

4. 複数選択

複数選択用のチェックボックス。

{
"id": "q4",
"type": "multiple-choice",
"label": "Which products are you interested in?",
"options": [
{ "value": "product_a", "label": "Product A" },
{ "value": "product_b", "label": "Product B" },
{ "value": "product_c", "label": "Product C" }
],
"validation": {
"required": true
}
}

5. ドロップダウン

リストから選択するセレクトドロップダウン。

{
"id": "q5",
"type": "dropdown",
"label": "Select your country",
"options": [
{ "value": "jp", "label": "Japan" },
{ "value": "us", "label": "United States" },
{ "value": "uk", "label": "United Kingdom" }
],
"validation": {
"required": true
}
}

6. リニアスケール

評価回答用の数値スケール。

{
"id": "q6",
"type": "linear-scale",
"label": "How likely are you to recommend us?",
"min": 0,
"max": 10,
"minLabel": "Not likely",
"maxLabel": "Very likely",
"validation": {
"required": true
}
}

7. 評価

2〜10段階で設定可能な星/ハート/サムズアップ評価入力。

{
"id": "q7",
"type": "rating",
"label": "Rate your overall satisfaction",
"levels": 5,
"symbol": "star",
"validation": {
"required": true
}
}
プロパティデフォルト説明
levelsnumber5評価レベル数(2〜10)
symbol'star' | 'heart' | 'thumb''star'評価表示に使用するシンボル

8. 日付

設定可能なオプション付きの日付選択ピッカー。

{
"id": "q8",
"type": "date",
"label": "When did you first use our service?",
"includeTime": false,
"includeYear": true,
"validation": {
"required": false
}
}
プロパティデフォルト説明
includeTimebooleanfalse日付と一緒に時間選択を含める
includeYearbooleantrue日付選択に年を含める

9. 時間

時間または期間入力用のタイムピッカー。

{
"id": "q9",
"type": "time",
"label": "What time works best for a callback?",
"answerType": "time",
"validation": {
"required": false
}
}
プロパティデフォルト説明
answerType'time' | 'duration''time'入力モード:特定の時間または期間

カスタムフック

内部フック

以下に記載されているフック(useSurveyTemplatesuseEditSurveyTemplateuseDeleteSurveyTemplate)は、ページコンポーネントで使用される内部フックです。メインパッケージのインデックスからはエクスポートされておらず、直接インポートすることはできません。表示されているインポートパスは説明目的のみです。通常のユースケースでは、代わりにページコンポーネント(SurveyTemplatePageEditSurveyTemplatePage)を使用してください。

useSurveyTemplates

ページネーションと検索機能を備えたアンケートテンプレートの取得と管理。

// IMPORTANT: This hook is internal and cannot be imported directly. (重要: このフックは内部用であり、直接インポートできません)
// This code example is for reference only to show the hook's interface. (このコード例はフックのインターフェースを示すための参考用です)
// Use SurveyTemplatePage component instead for standard use cases. (通常のユースケースでは代わりにSurveyTemplatePageコンポーネントを使用してください)

function TemplateList() {
const {
surveys, // Array of survey templates (SurveyTemplateDataEntity[]) (アンケートテンプレートの配列)
totalItems, // Total number of templates (テンプレートの総数)
isLoading,
error, // Error | null (エラーまたはnull)
refetch // () => Promise<void> - Function to refresh the list (リストを更新する関数)
} = useSurveyTemplates({
page: 1,
pageSize: 10,
keyword: "", // Optional: search keyword (オプション:検索キーワード)
orderBy: "createdAt", // Optional: sort field (オプション:ソートフィールド)
orderType: "desc" // Optional: sort direction ('asc' | 'desc') (オプション: ソート順)
});

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
<div>
<p>合計: {totalItems}</p>
<ul>
{surveys.map((survey) => (
<li key={survey.id}>{survey.name}</li>
))}
</ul>
</div>
);
}

useEditSurveyTemplate

スキーマ管理と送信処理を備えたアンケートテンプレート編集用フック。

// IMPORTANT: This hook is internal and cannot be imported directly. (重要: このフックは内部用であり、直接インポートできません)
// This code example is for reference only to show the hook's interface. (このコード例はフックのインターフェースを示すための参考用です)
// Use EditSurveyTemplatePage component instead for standard use cases. (通常のユースケースでは代わりにEditSurveyTemplatePageコンポーネントを使用してください)

function TemplateEditor({ id }: { id?: string }) {
const {
surveyData, // Current survey data from server (SurveyTemplateDataEntity | null) (サーバーからの現在のアンケートデータ)
currentSchema, // Current editable schema (SurveySchemaType | null) (現在の編集可能なスキーマ)
originalSchema, // Original schema for change detection (SurveySchemaType | null) (変更検出用の元のスキーマ)
isLoading,
isSubmitting,
error,
setCurrentSchema, // Function to update current schema (現在のスキーマを更新する関数)
handleCreateSurvey, // (schema: SurveySchemaType) => Promise<void> - Create new survey (新しいアンケートを作成)
handleUpdateSurvey, // (schema: SurveySchemaType) => Promise<void> - Update existing survey (既存のアンケートを更新)
retryFetchSurvey, // () => Promise<void> - Retry fetching survey data (アンケートデータの再取得)
isSchemaChanged, // boolean - True if schema differs from original (スキーマが元と異なる場合はtrue)
isButtonDisabled, // boolean - True if submit should be disabled (送信を無効にすべき場合はtrue)
submitButtonRef // React.RefObject<HTMLButtonElement> - Ref for submit button (送信ボタンのRef)
} = useEditSurveyTemplate({ id });

const handleSave = async () => {
if (!currentSchema) return;
if (id) {
await handleUpdateSurvey(currentSchema);
} else {
await handleCreateSurvey(currentSchema);
}
};

return (
<div>
{/* Editor UI (エディタUI) */}
<button
ref={submitButtonRef}
onClick={handleSave}
disabled={isButtonDisabled}
>
{isSubmitting ? "保存中..." : "保存"}
</button>
</div>
);
}

useDeleteSurveyTemplate

成功コールバック付きのアンケートテンプレート削除用フック。

// IMPORTANT: This hook is internal and cannot be imported directly. (重要: このフックは内部用であり、直接インポートできません)
// This code example is for reference only to show the hook's interface. (このコード例はフックのインターフェースを示すための参考用です)

function DeleteButton({ surveyId }: { surveyId: string }) {
const {
handleDeleteSurvey, // (id: string) => Promise<void> - Delete survey by ID (IDでアンケートを削除する関数)
isDeleting // boolean - True while deletion is in progress (削除中はtrue)
} = useDeleteSurveyTemplate({
onSuccess: () => {
console.log("Survey deleted successfully (アンケートが正常に削除されました)");
// Navigate back to list or refresh (リストに戻るか更新)
}
});

return (
<button
onClick={() => handleDeleteSurvey(surveyId)}
disabled={isDeleting}
>
削除
</button>
);
}

アンケートテンプレート構造

アンケートテンプレートはセクションヘッダーを含むフラットリスト構造を使用します:

interface SurveySchema {
title: string;
description?: string;
items: SurveyItem[]; // Flat list of section headers and questions (セクションヘッダーと質問のフラットリスト)
}

// Section header item - acts as a bookmark or page break (セクションヘッダー項目 - ブックマークまたはページ区切りとして機能)
interface SectionHeader {
id: string;
type: "section-header";
title: string;
description?: string;
action?: {
type: "submit";
} | {
type: "jump";
targetSectionId: string; // ID of another section-header for conditional branching (条件分岐用の別のsection-headerのID)
};
}

// Available question types (利用可能な質問タイプ)
type QuestionType =
| "short-text"
| "long-text"
| "single-choice"
| "multiple-choice"
| "dropdown"
| "linear-scale"
| "rating"
| "date"
| "time";

// Question item (質問項目)
interface Question {
id: string;
type: QuestionType; // short-text, long-text, single-choice, etc. (short-text、long-text、single-choiceなど)
label: string;
description?: string;
options?: QuestionOption[]; // For choice-based questions (選択ベースの質問用)
validation?: ValidationRules;
}

// Option for choice-based questions (選択ベースの質問用オプション)
interface QuestionOption {
value: string; // Unique value for the option (オプションの一意の値)
label: string; // Display label for the option (オプションの表示ラベル)
nextSectionId?: string; // ID of section to jump to when this option is selected (for conditional branching) (このオプションが選択されたときにジャンプするセクションのID(条件分岐用))
isOther?: boolean; // If true, shows a text input for custom "Other" response (trueの場合、カスタム「その他」回答用のテキスト入力を表示)
}

// Union of all item types (すべての項目タイプの共用体)
type SurveyItem = SectionHeader | Question;

アンケート構造の例:

{
"title": "Customer Feedback Survey",
"description": "Help us improve our service",
"items": [
{
"id": "section-intro",
"type": "section-header",
"title": "Introduction",
"description": "Please answer a few questions about yourself"
},
{
"id": "q1",
"type": "short-text",
"label": "What is your name?",
"validation": { "required": true }
},
{
"id": "section-feedback",
"type": "section-header",
"title": "Feedback",
"description": "Tell us about your experience"
},
{
"id": "q2",
"type": "linear-scale",
"label": "How satisfied are you?",
"min": 1,
"max": 10,
"minLabel": "Not satisfied",
"maxLabel": "Very satisfied",
"validation": { "required": true }
}
]
}

SurveyTemplateDataEntity型

SurveyTemplateDataEntity型は、実際のDynamoDBエンティティ構造を表します:

type SurveyTemplateDataEntity = {
// Primary keys (プライマリキー)
pk: string; // Partition key (パーティションキー)
sk: string; // Sort key (ソートキー)

// Entity identifiers (エンティティ識別子)
id: string; // Unique identifier (一意の識別子)
code: string; // Template code (テンプレートコード)
name: string; // Template name (テンプレート名)
version: number; // Version number (バージョン番号)
tenantCode: string; // Tenant code (テナントコード)
type: string; // Entity type (エンティティタイプ)

// Audit fields (as strings) (監査フィールド(文字列))
createdAt?: string; // Creation timestamp (作成日時)
updatedAt?: string; // Last update timestamp (最終更新日時)
createdBy?: string; // Creator user ID (作成者ユーザーID)
updatedBy?: string; // Last updater user ID (最終更新者ユーザーID)

// Optional fields (オプションフィールド)
cpk?: string; // Command partition key (コマンドパーティションキー)
csk?: string; // Command sort key (コマンドソートキー)
source?: string; // Source identifier (ソース識別子)
requestId?: string; // Request ID (リクエストID)
createdIp?: string; // Creator IP address (作成者IPアドレス)
updatedIp?: string; // Updater IP address (更新者IPアドレス)
seq?: number; // Sequence number (シーケンス番号)
ttl?: number; // Time to live (有効期限)
isDeleted?: boolean; // Soft delete flag (論理削除フラグ)

// Survey template data (アンケートテンプレートデータ)
attributes: {
description?: string; // Template description (テンプレート説明)
surveyTemplate: { // Survey template JSON structure (アンケートテンプレートJSON構造)
[key: string]: unknown;
};
};
}

バリデーションルール

バリデーションルールはvalidationオブジェクト内に判別共用体構造で定義されます:

// Base validation rules - applies to all question types (ベースバリデーションルール - すべての質問タイプに適用)
interface BaseValidationRules {
required?: boolean;
}

// For short-text questions (short-text質問用)
interface ShortTextValidationRules extends BaseValidationRules {
custom?: CustomValidationRule; // Supports all validation types (すべてのバリデーションタイプをサポート)
}

// For long-text questions (long-text質問用)
interface LongTextValidationRules extends BaseValidationRules {
custom?: LongTextValidationRule; // Supports only LengthValidation and RegexValidation (LengthValidationとRegexValidationのみサポート)
}

// For single-choice, dropdown, and multiple-choice questions (single-choice、dropdown、multiple-choice質問用)
interface ChoiceValidationRules extends BaseValidationRules {
shuffleOptions?: boolean; // Randomize option order (選択肢の順序をランダム化)
}

// For multiple-choice questions (複数選択質問用)
interface MultipleChoiceValidationRules extends ChoiceValidationRules {
custom?: MultipleChoiceValidationRule; // For min/max/exact selection count (最小/最大/正確な選択数用)
}

// Discriminated union for custom validation rules (カスタムバリデーションルール用の判別共用体)
// Note: short-text supports all validation types (注意: short-textはすべてのバリデーションタイプをサポート)
// Note: long-text only supports LengthValidation and RegexValidation (注意: long-textはLengthValidationとRegexValidationのみサポート)
type CustomValidationRule =
| NumberValidation // short-text only (short-textのみ)
| TextValidation // short-text only (short-textのみ)
| LengthValidation // short-text and long-text (short-textとlong-text)
| RegexValidation; // short-text and long-text (short-textとlong-text)

// For long-text questions - subset of CustomValidationRule (long-text質問用 - CustomValidationRuleのサブセット)
type LongTextValidationRule =
| LengthValidation
| RegexValidation;

interface NumberValidation {
type: "number";
rule: "gt" | "gte" | "lt" | "lte" | "eq" | "neq" | "between" | "not_between" | "is_number" | "is_whole";
value?: number;
value2?: number; // For 'between' and 'not_between' rules ('between'および'not_between'ルール用)
customError?: string;
}

interface TextValidation {
type: "text";
rule: "contains" | "not_contains" | "is_email" | "is_url";
value?: string;
customError?: string;
}

interface LengthValidation {
type: "length";
rule: "min" | "max";
value: number;
customError?: string;
}

interface RegexValidation {
type: "regex";
rule: "contains" | "not_contains" | "matches" | "not_matches";
value: string;
customError?: string;
}

// For multiple-choice questions (複数選択質問用)
interface MultipleChoiceValidationRule {
rule: "min" | "max" | "exact";
value: number;
customError?: string;
}

// Discriminated union for the validation property on Question (Questionのvalidationプロパティ用の判別共用体)
type ValidationRules =
| BaseValidationRules
| ShortTextValidationRules
| LongTextValidationRules
| ChoiceValidationRules
| MultipleChoiceValidationRules;

メールバリデーションの例:

{
"id": "email",
"type": "short-text",
"label": "Enter your email",
"validation": {
"required": true,
"custom": {
"type": "text",
"rule": "is_email",
"customError": "Please enter a valid email address"
}
}
}

数値範囲バリデーションの例:

{
"id": "age",
"type": "short-text",
"label": "Enter your age",
"validation": {
"required": true,
"custom": {
"type": "number",
"rule": "between",
"value": 18,
"value2": 120,
"customError": "Age must be between 18 and 120"
}
}
}

複数選択バリデーション(最小/最大選択数)の例:

{
"id": "interests",
"type": "multiple-choice",
"label": "Select your interests (2-5 choices)",
"options": [
{ "value": "sports", "label": "Sports" },
{ "value": "music", "label": "Music" },
{ "value": "reading", "label": "Reading" },
{ "value": "travel", "label": "Travel" },
{ "value": "cooking", "label": "Cooking" }
],
"validation": {
"required": true,
"custom": {
"rule": "min",
"value": 2,
"customError": "Please select at least 2 options"
}
}
}

環境変数

以下の環境変数を設定します:

変数説明必須
API_URLREST APIエンドポイントのベースURL(サーバーサイドのみ、NEXT_PUBLIC_API_URLより優先)いいえ
NEXT_PUBLIC_API_URLREST APIエンドポイントのベースURL(クライアントサイド)はい
NEXT_PUBLIC_TENANT_CODEx-tenant-codeヘッダー用のテナントコード(デフォルト:'common')いいえ
NEXT_PUBLIC_AWS_APPSYNC_GRAPHQLENDPOINTAWS AppSync GraphQLエンドポイントはい
NEXT_PUBLIC_AWS_APPSYNC_APIKEY認証用AWS AppSync APIキーはい
NEXT_PUBLIC_AWS_APPSYNC_REGIONAppSync用のAWSリージョンはい

.env.localの設定例:

# REST API Configuration (REST API設定)
# API_URL is optional (server-side only), NEXT_PUBLIC_API_URL is required (API_URLはオプション(サーバーサイドのみ)、NEXT_PUBLIC_API_URLは必須)
NEXT_PUBLIC_API_URL=https://api.example.com

# Tenant Configuration (テナント設定)
NEXT_PUBLIC_TENANT_CODE=my-tenant

# AWS AppSync Configuration (AWS AppSync設定)
NEXT_PUBLIC_AWS_APPSYNC_GRAPHQLENDPOINT=https://xxxxx.appsync-api.us-east-1.amazonaws.com/graphql
NEXT_PUBLIC_AWS_APPSYNC_APIKEY=da2-xxxxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_AWS_APPSYNC_REGION=us-east-1

スタイリング

アプリケーションでパッケージのスタイルをインポートします:

// In your layout or entry file (レイアウトまたはエントリファイルで)
import "@mbc-cqrs-serverless/survey-web/styles.css";

コンポーネントはTailwind CSSを使用しています。以下の要件でTailwind CSSが設定されていることを確認してください:

  • Tailwind CSS 3.x
  • tailwindcss-animateプラグイン

依存関係

このパッケージで使用される主要な依存関係:

  • React 18.x
  • Next.js 14.x
  • ドラッグ&ドロップ用@dnd-kit
  • Apollo Client
  • Radix UIコンポーネント
  • Tailwind CSS 3.x
  • react-hook-form
  • バリデーション用Zod
  • 日付処理用date-fns
  • アイコン用lucide-react

変更履歴

バージョン履歴

すべてのバージョン履歴とリリースノートはWebパッケージ変更履歴を参照してください。

関連ドキュメント