AIエディタを使ったVibe codingの実際の開発事例(ユースケース)
Lightning Web Componentの開発例
要件:顧客管理ダッシュボードの作成
自然言語での要求:
"取引先の情報を表示し、関連する商談と取引先責任者を
タブ形式で切り替えて表示できるダッシュボードコンポーネントを作成したい。
リアルタイムでデータを更新し、Excel出力機能も必要。"
AIエディタによる段階的実装
ステップ1: 基本構造の生成
<!-- accountDashboard.html -->
<template>
<lightning-card title="顧客管理ダッシュボード" icon-name="standard:account">
<div slot="actions">
<lightning-button
label="Excel出力"
onclick={exportToExcel}
variant="brand">
</lightning-button>
</div>
<lightning-tabset>
<lightning-tab label="取引先情報" value="account">
<c-account-detail account-id={accountId}></c-account-detail>
</lightning-tab>
<lightning-tab label="商談" value="opportunities">
<c-opportunity-list account-id={accountId}></c-opportunity-list>
</lightning-tab>
<lightning-tab label="取引先責任者" value="contacts">
<c-contact-list account-id={accountId}></c-contact-list>
</lightning-tab>
</lightning-tabset>
</lightning-card>
</template>
ステップ2: JavaScriptコントローラーの実装
// accountDashboard.js
import { LightningElement, api, wire, track } from 'lwc';
import { refreshApex } from '@salesforce/apex';
import getAccountData from '@salesforce/apex/AccountDashboardController.getAccountData';
import exportAccountData from '@salesforce/apex/AccountDashboardController.exportAccountData';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class AccountDashboard extends LightningElement {
@api accountId;
@track accountData;
wiredAccountData;
@wire(getAccountData, { accountId: '$accountId' })
wireAccountData(result) {
this.wiredAccountData = result;
if (result.data) {
this.accountData = result.data;
} else if (result.error) {
this.showToast('エラー', result.error.body.message, 'error');
}
}
async handleRefresh() {
try {
await refreshApex(this.wiredAccountData);
this.showToast('成功', 'データが更新されました', 'success');
} catch (error) {
this.showToast('エラー', 'データの更新に失敗しました', 'error');
}
}
async exportToExcel() {
try {
const result = await exportAccountData({ accountId: this.accountId });
// ファイルダウンロード処理
const link = document.createElement('a');
link.href = 'data:application/vnd.ms-excel;base64,' + result;
link.download = `account_${this.accountId}_data.xlsx`;
link.click();
this.showToast('成功', 'Excelファイルをダウンロードしました', 'success');
} catch (error) {
this.showToast('エラー', 'Excel出力に失敗しました', 'error');
}
}
showToast(title, message, variant) {
this.dispatchEvent(new ShowToastEvent({
title: title,
message: message,
variant: variant
}));
}
}
ステップ3: Apexコントローラーの実装
public with sharing class AccountDashboardController {
@AuraEnabled(cacheable=true)
public static AccountDataWrapper getAccountData(Id accountId) {
try {
if (!Schema.sObjectType.Account.isAccessible()) {
throw new AuraHandledException('取引先への読み取り権限がありません');
}
Account account = [
SELECT Id, Name, BillingAddress, Phone, Website,
AnnualRevenue, NumberOfEmployees, Industry
FROM Account
WHERE Id = :accountId
LIMIT 1
];
List<Opportunity> opportunities = [
SELECT Id, Name, StageName, Amount, CloseDate
FROM Opportunity
WHERE AccountId = :accountId
ORDER BY CloseDate DESC
LIMIT 10
];
List<Contact> contacts = [
SELECT Id, Name, Email, Phone, Title
FROM Contact
WHERE AccountId = :accountId
ORDER BY LastModifiedDate DESC
LIMIT 20
];
return new AccountDataWrapper(account, opportunities, contacts);
} catch (Exception e) {
throw new AuraHandledException('データの取得に失敗しました: ' + e.getMessage());
}
}
@AuraEnabled
public static String exportAccountData(Id accountId) {
try {
AccountDataWrapper data = getAccountData(accountId);
// Excel生成ロジック(簡略化)
String csvData = generateCSVData(data);
return EncodingUtil.base64Encode(Blob.valueOf(csvData));
} catch (Exception e) {
throw new AuraHandledException('Excel出力に失敗しました: ' + e.getMessage());
}
}
private static String generateCSVData(AccountDataWrapper data) {
List<String> csvLines = new List<String>();
// ヘッダー
csvLines.add('タイプ,名前,ステージ/タイトル,金額/メール,完了日/電話');
// 取引先データ
csvLines.add(String.format('取引先,{0},{1},{2},{3}',
new String[]{
data.account.Name,
data.account.Industry,
String.valueOf(data.account.AnnualRevenue),
data.account.Phone
}));
// 商談データ
for (Opportunity opp : data.opportunities) {
csvLines.add(String.format('商談,{0},{1},{2},{3}',
new String[]{
opp.Name,
opp.StageName,
String.valueOf(opp.Amount),
String.valueOf(opp.CloseDate)
}));
}
return String.join(csvLines, '\n');
}
public class AccountDataWrapper {
@AuraEnabled public Account account { get; set; }
@AuraEnabled public List<Opportunity> opportunities { get; set; }
@AuraEnabled public List<Contact> contacts { get; set; }
public AccountDataWrapper(Account acc, List<Opportunity> opps, List<Contact> cons) {
this.account = acc;
this.opportunities = opps;
this.contacts = cons;
}
}
}
Apex クラスの実装例
要件:複雑な承認プロセスの自動化
自然言語での要求:
"商談の金額に応じて、異なる承認者ルートを自動設定し、
承認状況をリアルタイムで追跡できるシステムを実装したい。
承認者の不在時は代理承認者に自動エスカレーションする機能も必要。"
AIエディタによる実装
public class OpportunityApprovalManager {
// 承認ルール設定
private static final Map<Decimal, String> APPROVAL_RULES = new Map<Decimal, String>{
100000 => 'MANAGER_APPROVAL',
500000 => 'DIRECTOR_APPROVAL',
1000000 => 'VP_APPROVAL',
5000000 => 'EXECUTIVE_APPROVAL'
};
@InvocableMethod(label='承認プロセス開始')
public static List<ApprovalResult> initiateApprovalProcess(List<ApprovalRequest> requests) {
List<ApprovalResult> results = new List<ApprovalResult>();
for (ApprovalRequest request : requests) {
try {
ApprovalResult result = processOpportunityApproval(request.opportunityId);
results.add(result);
} catch (Exception e) {
ApprovalResult errorResult = new ApprovalResult();
errorResult.success = false;
errorResult.message = 'エラー: ' + e.getMessage();
errorResult.opportunityId = request.opportunityId;
results.add(errorResult);
}
}
return results;
}
private static ApprovalResult processOpportunityApproval(Id opportunityId) {
// 商談データの取得
Opportunity opp = [
SELECT Id, Name, Amount, OwnerId, StageName, Account.OwnerId
FROM Opportunity
WHERE Id = :opportunityId
LIMIT 1
];
// 承認レベルの決定
String approvalLevel = determineApprovalLevel(opp.Amount);
// 承認者の特定
List<Id> approvers = getApprovers(opp, approvalLevel);
// 承認プロセスの作成
Approval.ProcessSubmitRequest req = new Approval.ProcessSubmitRequest();
req.setComments('システムによる自動承認申請');
req.setObjectId(opportunityId);
req.setSubmitterId(UserInfo.getUserId());
req.setProcessDefinitionNameOrId('OpportunityApprovalProcess');
req.setSkipEntryCriteria(false);
// 承認申請の実行
Approval.ProcessResult result = Approval.process(req);
// 結果の作成
ApprovalResult approvalResult = new ApprovalResult();
approvalResult.success = result.isSuccess();
approvalResult.opportunityId = opportunityId;
approvalResult.processInstanceId = result.getInstanceId();
approvalResult.message = result.isSuccess() ?
'承認プロセスが開始されました' :
'承認プロセスの開始に失敗しました: ' + String.join(result.getErrors(), ', ');
// 承認状況の追跡レコード作成
createApprovalTracking(opp, approvers, result.getInstanceId());
return approvalResult;
}
private static String determineApprovalLevel(Decimal amount) {
if (amount == null) return 'MANAGER_APPROVAL';
List<Decimal> thresholds = new List<Decimal>(APPROVAL_RULES.keySet());
thresholds.sort();
for (Decimal threshold : thresholds) {
if (amount <= threshold) {
return APPROVAL_RULES.get(threshold);
}
}
return 'EXECUTIVE_APPROVAL';
}
private static List<Id> getApprovers(Opportunity opp, String approvalLevel) {
List<Id> approvers = new List<Id>();
// ユーザーの役職情報を取得
User owner = [
SELECT Id, ManagerId, UserRole.Name, Department
FROM User
WHERE Id = :opp.OwnerId
LIMIT 1
];
switch on approvalLevel {
when 'MANAGER_APPROVAL' {
if (owner.ManagerId != null) {
approvers.add(owner.ManagerId);
}
}
when 'DIRECTOR_APPROVAL' {
// 部門ディレクターを検索
List<User> directors = [
SELECT Id FROM User
WHERE UserRole.Name LIKE '%Director%'
AND Department = :owner.Department
AND IsActive = true
LIMIT 3
];
for (User director : directors) {
approvers.add(director.Id);
}
}
when 'VP_APPROVAL' {
// VP以上の役職者を検索
List<User> vps = [
SELECT Id FROM User
WHERE (UserRole.Name LIKE '%VP%' OR UserRole.Name LIKE '%Vice President%')
AND IsActive = true
LIMIT 5
];
for (User vp : vps) {
approvers.add(vp.Id);
}
}
when 'EXECUTIVE_APPROVAL' {
// 役員クラスを検索
List<User> executives = [
SELECT Id FROM User
WHERE UserRole.Name LIKE '%Executive%'
OR UserRole.Name LIKE '%CEO%'
OR UserRole.Name LIKE '%President%'
AND IsActive = true
LIMIT 5
];
for (User exec : executives) {
approvers.add(exec.Id);
}
}
}
return approvers;
}
private static void createApprovalTracking(Opportunity opp, List<Id> approvers, Id processInstanceId) {
List<ApprovalTracking__c> trackingRecords = new List<ApprovalTracking__c>();
for (Id approverId : approvers) {
ApprovalTracking__c tracking = new ApprovalTracking__c();
tracking.Opportunity__c = opp.Id;
tracking.Approver__c = approverId;
tracking.ProcessInstanceId__c = processInstanceId;
tracking.Status__c = 'Pending';
tracking.CreatedDate__c = System.now();
trackingRecords.add(tracking);
}
if (!trackingRecords.isEmpty()) {
insert trackingRecords;
}
}
// 代理承認者への自動エスカレーション
@future
public static void checkForEscalation(Set<Id> processInstanceIds) {
List<ProcessInstance> processes = [
SELECT Id, Status, CreatedDate, TargetObjectId
FROM ProcessInstance
WHERE Id IN :processInstanceIds
AND Status = 'Pending'
];
for (ProcessInstance process : processes) {
// 24時間経過した場合のエスカレーション
if (process.CreatedDate.addHours(24) < System.now()) {
escalateToDelegate(process.Id);
}
}
}
private static void escalateToDelegate(Id processInstanceId) {
// 現在の承認者を取得
List<ProcessInstanceWorkitem> workitems = [
SELECT Id, ActorId, ProcessInstanceId
FROM ProcessInstanceWorkitem
WHERE ProcessInstanceId = :processInstanceId
];
for (ProcessInstanceWorkitem workitem : workitems) {
// 代理承認者を検索
List<User> delegates = [
SELECT Id FROM User
WHERE ManagerId = :workitem.ActorId
AND IsActive = true
LIMIT 1
];
if (!delegates.isEmpty()) {
// 代理承認者に再アサイン
Approval.ProcessWorkitemRequest req = new Approval.ProcessWorkitemRequest();
req.setComments('自動エスカレーション: 代理承認者へのアサイン');
req.setAction('Reassign');
req.setWorkitemId(workitem.Id);
req.setNewActorIds(new List<Id>{delegates[0].Id});
Approval.process(req);
}
}
}
// データクラス
public class ApprovalRequest {
@InvocableVariable(required=true)
public Id opportunityId;
}
public class ApprovalResult {
@InvocableVariable
public Boolean success;
@InvocableVariable
public String message;
@InvocableVariable
public Id opportunityId;
@InvocableVariable
public Id processInstanceId;
}
}
テストコードの自動生成
AIエディタによるテストクラスの生成
@isTest
public class OpportunityApprovalManagerTest {
@TestSetup
static void setupTestData() {
// テスト用のユーザー作成
Profile profile = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1];
User manager = new User(
FirstName = 'Test',
LastName = 'Manager',
Email = 'testmanager@example.com',
Username = 'testmanager@example.com.test',
Alias = 'tmgr',
ProfileId = profile.Id,
TimeZoneSidKey = 'Asia/Tokyo',
LocaleSidKey = 'ja_JP',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'ja'
);
insert manager;
User salesUser = new User(
FirstName = 'Test',
LastName = 'Sales',
Email = 'testsales@example.com',
Username = 'testsales@example.com.test',
Alias = 'tsales',
ProfileId = profile.Id,
ManagerId = manager.Id,
TimeZoneSidKey = 'Asia/Tokyo',
LocaleSidKey = 'ja_JP',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'ja'
);
insert salesUser;
// テスト用の取引先作成
Account testAccount = new Account(
Name = 'テスト取引先',
OwnerId = salesUser.Id
);
insert testAccount;
// テスト用の商談作成
List<Opportunity> opportunities = new List<Opportunity>();
// 小額商談(マネージャー承認)
opportunities.add(new Opportunity(
Name = 'テスト商談 - 小額',
AccountId = testAccount.Id,
Amount = 50000,
CloseDate = Date.today().addDays(30),
StageName = 'Prospecting',
OwnerId = salesUser.Id
));
// 中額商談(ディレクター承認)
opportunities.add(new Opportunity(
Name = 'テスト商談 - 中額',
AccountId = testAccount.Id,
Amount = 300000,
CloseDate = Date.today().addDays(30),
StageName = 'Prospecting',
OwnerId = salesUser.Id
));
// 高額商談(VP承認)
opportunities.add(new Opportunity(
Name = 'テスト商談 - 高額',
AccountId = testAccount.Id,
Amount = 800000,
CloseDate = Date.today().addDays(30),
StageName = 'Prospecting',
OwnerId = salesUser.Id
));
insert opportunities;
}
@isTest
static void testManagerApprovalProcess() {
Opportunity smallOpp = [
SELECT Id FROM Opportunity
WHERE Name = 'テスト商談 - 小額'
LIMIT 1
];
Test.startTest();
List<OpportunityApprovalManager.ApprovalRequest> requests =
new List<OpportunityApprovalManager.ApprovalRequest>();
OpportunityApprovalManager.ApprovalRequest request =
new OpportunityApprovalManager.ApprovalRequest();
request.opportunityId = smallOpp.Id;
requests.add(request);
List<OpportunityApprovalManager.ApprovalResult> results =
OpportunityApprovalManager.initiateApprovalProcess(requests);
Test.stopTest();
// 結果の検証
System.assertEquals(1, results.size(), '結果が1件返されること');
System.assertEquals(true, results[0].success, '承認プロセスが成功すること');
System.assertEquals(smallOpp.Id, results[0].opportunityId, '商談IDが正しく設定されること');
// 追跡レコードの確認
List<ApprovalTracking__c> trackings = [
SELECT Id, Status__c
FROM ApprovalTracking__c
WHERE Opportunity__c = :smallOpp.Id
];
System.assertNotEquals(0, trackings.size(), '追跡レコードが作成されること');
}
@isTest
static void testDirectorApprovalProcess() {
Opportunity mediumOpp = [
SELECT Id FROM Opportunity
WHERE Name = 'テスト商談 - 中額'
LIMIT 1
];
Test.startTest();
List<OpportunityApprovalManager.ApprovalRequest> requests =
new List<OpportunityApprovalManager.ApprovalRequest>();
OpportunityApprovalManager.ApprovalRequest request =
new OpportunityApprovalManager.ApprovalRequest();
request.opportunityId = mediumOpp.Id;
requests.add(request);
List<OpportunityApprovalManager.ApprovalResult> results =
OpportunityApprovalManager.initiateApprovalProcess(requests);
Test.stopTest();
// 結果の検証
System.assertEquals(1, results.size(), '結果が1件返されること');
System.assertEquals(mediumOpp.Id, results[0].opportunityId, '商談IDが正しく設定されること');
}
@isTest
static void testVPApprovalProcess() {
Opportunity largeOpp = [
SELECT Id FROM Opportunity
WHERE Name = 'テスト商談 - 高額'
LIMIT 1
];
Test.startTest();
List<OpportunityApprovalManager.ApprovalRequest> requests =
new List<OpportunityApprovalManager.ApprovalRequest>();
OpportunityApprovalManager.ApprovalRequest request =
new OpportunityApprovalManager.ApprovalRequest();
request.opportunityId = largeOpp.Id;
requests.add(request);
List<OpportunityApprovalManager.ApprovalResult> results =
OpportunityApprovalManager.initiateApprovalProcess(requests);
Test.stopTest();
// 結果の検証
System.assertEquals(1, results.size(), '結果が1件返されること');
System.assertEquals(largeOpp.Id, results[0].opportunityId, '商談IDが正しく設定されること');
}
@isTest
static void testErrorHandling() {
Test.startTest();
List<OpportunityApprovalManager.ApprovalRequest> requests =
new List<OpportunityApprovalManager.ApprovalRequest>();
OpportunityApprovalManager.ApprovalRequest request =
new OpportunityApprovalManager.ApprovalRequest();
request.opportunityId = null; // 不正なID
requests.add(request);
List<OpportunityApprovalManager.ApprovalResult> results =
OpportunityApprovalManager.initiateApprovalProcess(requests);
Test.stopTest();
// エラーハンドリングの検証
System.assertEquals(1, results.size(), '結果が1件返されること');
System.assertEquals(false, results[0].success, 'エラーが正しく処理されること');
System.assert(results[0].message.contains('エラー'), 'エラーメッセージが設定されること');
}
@isTest
static void testBulkProcessing() {
List<Opportunity> allOpps = [SELECT Id FROM Opportunity];
Test.startTest();
List<OpportunityApprovalManager.ApprovalRequest> requests =
new List<OpportunityApprovalManager.ApprovalRequest>();
for (Opportunity opp : allOpps) {
OpportunityApprovalManager.ApprovalRequest request =
new OpportunityApprovalManager.ApprovalRequest();
request.opportunityId = opp.Id;
requests.add(request);
}
List<OpportunityApprovalManager.ApprovalResult> results =
OpportunityApprovalManager.initiateApprovalProcess(requests);
Test.stopTest();
// バルク処理の検証
System.assertEquals(allOpps.size(), results.size(), '全ての商談が処理されること');
for (OpportunityApprovalManager.ApprovalResult result : results) {
System.assertNotEquals(null, result.opportunityId, '商談IDが設定されること');
}
}
}
6. 注意点と課題
セキュリティ考慮事項
AIエディタ使用時のセキュリティリスク
1. 機密情報の漏洩リスク AIエディタにコードを送信する際、顧客情報や機密データが含まれる可能性があります。以下の対策が必要です:
- 実際の顧客データを含むコードをAIエディタに送信しない
- テストデータやサンプルデータを使用してコード生成を行う
- 社内のデータ分類ポリシーに従った利用ガイドラインの策定
- AIエディタの利用ログの監査と定期的なレビュー
2. 生成されたコードのセキュリティ脆弱性 AIが生成したコードには、セキュリティ上の問題が含まれる可能性があります:
// 危険な例:SQLインジェクション脆弱性
public static List<Account> searchAccounts(String searchTerm) {
// AIが生成した危険なコード例
String query = 'SELECT Id, Name FROM Account WHERE Name LIKE \'%' + searchTerm + '%\'';
return Database.query(query); // SQLインジェクション脆弱性
}
// 安全な実装例
public static List<Account> searchAccountsSafe(String searchTerm) {
String safeSearchTerm = '%' + String.escapeSingleQuotes(searchTerm) + '%';
return [SELECT Id, Name FROM Account WHERE Name LIKE :safeSearchTerm];
}
3. 権限とアクセス制御 AIが生成するコードが適切な権限チェックを含んでいるか確認が必要です:
// 権限チェックを含む安全なコード例
public static List<Account> getAccounts() {
// オブジェクトレベルの権限チェック
if (!Schema.sObjectType.Account.isAccessible()) {
throw new AuraHandledException('取引先への読み取り権限がありません');
}
// フィールドレベルの権限チェック
if (!Schema.sObjectType.Account.fields.Name.isAccessible()) {
throw new AuraHandledException('取引先名フィールドへの読み取り権限がありません');
}
return [SELECT Id, Name FROM Account WITH SECURITY_ENFORCED LIMIT 100];
}
コード品質の担保方法
1. AIによるコード生成後の必須チェック項目
Salesforce固有の制約チェック
- ガバナーリミット(SOQL実行回数、DML操作回数、ヒープサイズなど)への対応
- バルク処理の実装
- 適切な例外処理の実装
- トランザクション管理の考慮
// 良い例:ガバナーリミットを考慮したコード
public class ContactProcessor {
public static void processContacts(List<Contact> contacts) {
List<Contact> contactsToUpdate = new List<Contact>();
// バルク処理でSOQL実行回数を最小化
Map<Id, Account> accountMap = new Map<Id, Account>([
SELECT Id, Industry
FROM Account
WHERE Id IN (SELECT AccountId FROM Contact WHERE Id IN :contacts)
]);
for (Contact contact : contacts) {
if (accountMap.containsKey(contact.AccountId)) {
contact.Industry__c = accountMap.get(contact.AccountId).Industry;
contactsToUpdate.add(contact);
}
}
// バルクDML操作
if (!contactsToUpdate.isEmpty()) {
try {
Database.update(contactsToUpdate, false);
} catch (DmlException e) {
System.debug('DML Error: ' + e.getMessage());
// 適切なエラーハンドリング
}
}
}
}
2. 静的コード解析ツールの活用
- PMD for Salesforce
- SonarQube Salesforce Plugin
- Salesforce Code Analyzer
3. コードレビュープロセスの強化 AIが生成したコードについては、特に以下の観点でのレビューを強化:
- ビジネスロジックの正確性
- パフォーマンスの最適化
- セキュリティ要件の満足
- 保守性とテスタビリティ
チーム開発での運用ポイント
1. AIエディタ利用のガイドライン策定
利用ルールの明文化
## AIエディタ利用ガイドライン
### 基本方針
1. 本番データを含むコードをAIエディタに送信しない
2. 生成されたコードは必ず動作確認とセキュリティチェックを行う
3. AIの提案をそのまま採用せず、チームの規約に合わせて調整する
### 利用可能な範囲
- ボイラープレートコードの生成
- テストコードの初期実装
- リファクタリングの提案
- ドキュメンテーションの作成
### 利用禁止事項
- 本番環境の機密データを含むコードの送信
- 未検証のコードの直接的な本番環境への適用
- ライセンスが不明なコードの利用
2. 品質基準の統一
コード品質チェックリスト
- Salesforceガバナーリミットへの対応
- 適切な例外処理の実装
- セキュリティ要件の満足
- テストカバレッジ75%以上
- コメントとドキュメンテーションの充実
- チームのコーディング規約への準拠
3. 知識共有とベストプラクティスの蓄積
効果的なプロンプトの共有 チーム内で効果的だったプロンプトや手法を共有し、組織全体のVibe Coding能力を向上させます。
## 有効なプロンプト例集
### Apexクラス生成
「Salesforceのガバナーリミットを考慮し、バルク処理に対応した
[機能名]のApexクラスを作成してください。エラーハンドリングと
ログ出力も含めて実装してください。」
### テストクラス生成
「先ほど作成したApexクラスに対して、以下の条件を満たす
テストクラスを作成してください:
- 正常系・異常系の両方をテスト
- バルク処理のテスト
- 75%以上のカバレッジ確保」
4. 継続的な改善プロセス
定期的な振り返り
- 月次でのAIエディタ利用状況のレビュー
- 問題点や改善点の特定と対策の検討
- 新機能や新しい手法の評価と導入検討
メトリクスの計測
- 開発速度の向上度
- コード品質の指標(バグ発生率、レビュー指摘事項数)
- 開発者の満足度
- 学習コストの変化